Rails

【Rails】Fine Tuningモデルを自由に選択できる自動応答機能をRails7で作成してみた

Rails

こんにちは、千葉(@yoshi_chibaaa)です。

今回はOpenAIのAPIを用いて、Fine Tuningモデルを自由に選択できる自動応答機能を作ってみたので紹介したいと思います。Fine Tuningモデルはトークン数によって費用が発生しますが、アカウント登録から3ヶ月は18ドル分のクレジットが付与されるため、その期間は無料で利用することができます。

OpenAIを使用するには、OpenAIのAPIキーが必要になります。未取得の場合はこちらから新規登録と取得を行ってください。

Fine Tuningモデルとは

Fine-tuningモデルは、事前に大規模なデータセットで訓練された一般的な言語モデルを、特定のタスクやドメインに合わせて追加の訓練を行い、特定のタスクに特化したモデルを作成する手法です。

Fine-tuningモデルは、一般的な言語モデル(プリトレーニングモデル)が大規模なテキストデータセットで学習された後に、タスク固有のデータセットを使用して再訓練されます。この再訓練により、モデルは特定のタスクやドメインに関する知識を獲得し、そのタスクに特化した予測や解析を行うことができるようになります。

Fine-tuningモデルは、特定のタスクに対して高い性能を発揮し、タスク固有のデータや特徴に適合したモデルを構築することができます。例えば、質問応答、感情分析、文章生成などのタスクに対して、Fine-tuningモデルは高い精度や性能を発揮することができます。また、Fine-tuningモデルは、プリトレーニングモデルが持つ幅広い言語理解の能力を引き継ぎながら、特定のタスクに関するドメイン知識を獲得することができるため、効率的なモデル構築手法として広く利用されています。

FineTuningモデルとは

利用料金

FineTuningモデル料金
モデル学習使用
Ada$0.0004 / 1K tokens$0.0016 / 1K tokens
Babbage$0.0006 / 1K tokens$0.0024 / 1K tokens
Curie$0.0030 / 1K tokens$0.0120 / 1K tokens
Davinci$0.0300 / 1K tokens$0.1200 / 1K tokens

実装手順

今回は以下のYouTube動画を元にアプリケーションを構築していきます。

新規アプリケーションの作成

以下のコマンドで新規アプリケーションを立ち上げます。

rails new時に-j esbuildオプションを指定し、JavaScriptのビルドツールをesbuildに設定しています。また、-c bootstrapオプションでBootstrapも使用できるように設定しています。

% rails new chatgpt-demo -d mysql -j esbuild -c bootstrap
% cd chatgpt-demo
% rails db:create
  • Ruby: 3.2.2
  • Rails: 7.0.7
  • DB: MySQL

pagesコントローラの作成

% rails g controller pages home

faraday gemの導入

HTTP通信ライブラリであるGem faradayをインストールします。

% bundle add faraday
Rails
【Rails】HTTPクライアントライブラリ「Faraday」の使い方まとめ 今回はHTTPクライアントライブラリであるFaradayの導入と使い方について簡単にまとめました。RubyにはHTTPク...

foreman Gemの追加

開発環境セットアップ用のGem foremanをインストールします。

# Gemfile

gem "foreman", github: "ddollar/foreman"
% bundle install

gem "foreman", github: "ddollar/foreman"は、開発環境においてプロセス管理を行うためのGemです。複数のサーバーやプロセスを一度に起動・管理し、開発環境のセットアップやデバッグを簡便にするために使用されます。

例えば、foreman Gemを使用することでWebサーバーを起動すると同時に、バックグラウンドで動作するプロセスも一緒に起動できます。これによりアプリケーションのセットアップが簡単になります。

参考: Forman

AiRequestJobの作成

バックグラウンド処理を行うAiRequestJobというジョブを作成します。

% rails g job AiRequestJob

動作確認1

ここまでで一度サーバを起動し、Railsの初期画面が表示されるか確認します。

% bin/dev
Raiks初期画面

routesの修正

ルーティングを以下のように修正します。

Rails.application.routes.draw do
  post 'ai_request', to: 'pages#ai_request'
  root 'pages#home'
end

ビューの作成

aiディレクトリを作成し、その中に2つの部分テンプレートを作成します。

% mkdir app/views/ai
% touch app/views/ai/_output.html.erb
% touch app/views/ai/_request_form.html.erb 
ファイル構成

uuidの生成

PagesControllerhomeメソッド内でランダムなUUIDを生成します。

class PagesController < ApplicationController
  require 'securerandom'

  def home
    @uuid = SecureRandom.uuid
  end
end

require 'securerandom'securerandomという標準ライブラリを読み込んでいます。SecureRandomモジュールは、ランダムな文字列や数値を生成するために使用されるセキュアな乱数生成機能を提供します。

home.html.erbの修正

トップページであるhome.html.erbを以下のように編集します。

このページには、質問の入力欄(ai/request_form)とその出力結果を表示する欄(ai/output)を部分テンプレートを用いて実装しています。

<div class="container mt-5">
  <h1>Pages#home</h1>
  <p>Find me in app/views/pages/home.html.erb</p>
  <%= render partial: "ai/request_form", locals: { uuid: @uuid } %>

  <%= turbo_stream_from "channel_#{@uuid}" %>

  <div id="ai_output">
    <%= render partial: "ai/output", locals: { generated_idea: "" } %>
  </div>
</div>
  • <%= render partial: "ai/request_form", locals: { uuid: @uuid } %>
    ai/request_formという部分テンプレートをレンダリングし、その際にuuidというローカル変数が渡しています。この変数はPagesControllerに定義した@uuidインスタンス変数の値を参照しています。
  • <%= turbo_stream_from "channel_#{@uuid}" %>
    Turbo Streamsを使用してリアルタイムな更新を可能にするためのコードです。指定されたchannel_#{@uuid}というチャンネルからのストリームを受信し、自動的に更新します。これにより、ページの一部を非同期で更新することができます。
  • <%= render partial: "ai/output", locals: { generated_idea: "" } %>
    ai/outputという部分テンプレートをレンダリングし、その際にgenerated_ideaという空のローカル変数を渡しています。

_request_form.html.erbの修正

質問入力フォームの部分テンプレートになります。

使用するモデルもここで選択できるようセレクトボックスを実装しています。

<%= form_for :ai_request, url: ai_request_path, method: :post, remote: true do |f| %>
  <%= f.hidden_field :uuid, value: @uuid %>
  <div class="form-group">
    <%= f.text_area :prompt, class: "form-control" %>
  </div>
  <%= f.select :ai_model, options_for_select([
        [ "Ada $0.0016 / 1K tokens", "ada" ], 
        [ "Babbage $0.0024 / 1K tokens", "babbage" ], 
        [ "Curie $0.0120 / 1K tokens", "curie" ],
        [ "Davinci $0.1200 / 1K tokens", "davinci" ],
    ], "davinci"), {}, class:"form-control" %>
  <%= f.submit "Send", class: "btn btn-primary" %>
<% end %>
  • <%= form_for :ai_request, url: ai_request_path, method: :post, remote: true do |f| %>
    ai_request_pathというパスに対してHTTP POSTリクエストを行っています。remote: trueはAjaxを使用して非同期通信を行うことを示しています。
  • <%= f.hidden_field :uuid, value: @uuid %>
    :uuidという属性名に対して、インスタンス変数@uuidの値をhidden_fieldを用いて送信しています。
  • <%= f.select :ai_model, options_for_select([...], "davinci"), {}, class:"form-control" %>
    :ai_modelという属性名に対して、選択肢のリストを配列として渡しています。初期選択値として"davinci"を指定しています。

_output.html.erbの修正

出力結果の部分テンプレートになります。

generated_idea値の有無で、表示・非表示を分けるため以下のように修正します。

<div class="card">
  <% if generated_idea %>
    <%= content_tag(:div, generated_idea) %>
  <% end %>
</div>

APIキーの設定

OpenAIのAPIキーを設定します。

APIキーを未取得の場合は、こちらから新規登録と取得を行ってください。

% EDITOR=vi rails credentials:edit --environment development
openai:
  api_key: <OpenAI APIキー>
Rails
【Rails】秘匿情報を環境毎に管理することができるMulti Environment Credentialsについて初心者向けにまとめてみた今回は秘匿情報を環境毎に管理することができるMulti Environment Credentialsについて簡単にまとめたので紹介しています。付与されている権限によって閲覧できる情報を管理することができるため、権限の異なる複数人で開発する際にはもってこいの機能になります。...

pagesコントローラの修正

先ほど設定したOpenAIのAPIキーと入力された質問内容を元にAIRequestJobを実行するため、以下のようにPagesControllerを編集します。

class PagesController < ApplicationController
  require 'securerandom'

  before_action :set_api_key, only: [:ai_request]

  def home
    @uuid = SecureRandom.uuid
  end

  def ai_request
    AiRequestJob.perform_later(ai_request_params, @api_key)
  end

  private

  def ai_request_params
    params.require(:ai_request).permit(:prompt, :ai_model, :uuid)
  end

  def set_api_key
    @api_key = Rails.application.credentials.dig(:openai, :api_key)
  end
end
Rails
【Rails】配列・ハッシュから安全に値を取り出すことができるdigメソッドについて配列とハッシュから安全に値を取り出すことができるdigメソッドについて紹介しています。配列では「Arrayオブジェクト[index]」のように、ハッシュからは「ハッシュオブジェクト[キー]」のようにも値を取得することができますが、digメソッドで取得する際の違いはあるのでしょうか。...

AiRequestJobの修正

AiRequestJobの中身を以下のように修正します。

ここでは与えられたパラメータに基づいてTurbo Streamsを使用して、特定のチャンネルにデータを送信し、ビューの一部を非同期で更新しています。

class AiRequestJob < ApplicationJob
  queue_as :default

  def perform(ai_request_params, api_key)
    uuid = ai_request_params[:uuid]
    generated_idea = ai_request_params[:prompt]

    Turbo::StreamsChannel.broadcast_replace_to("channel_#{uuid}",
      target: 'ai_output',
      partial: 'ai/output',
      locals: { generated_idea:  }
    )
  end
end
  • Turbo::StreamsChannel.broadcast_replace_to("channel_#{uuid}"
    Turbo Streamsを使用して、"channel_#{uuid}"というチャンネルにデータを送信しています。uuidはチャンネルの識別に使用しています。
  • target: 'ai_output'
    targetでデータを送信するターゲットを指定しています。
  • partial: 'ai/output'
    partialでは更新される部分のビューファイルを指定しています。
  • locals: { generated_idea: }
    localsでは部分ビュー内で使用されるローカル変数を指定します。

動作確認2

ここまでで入力内容が正常に反映されるかチェックしてみます。

Image from Gyazo

AiRequestJobの修正

現状、新たに投稿内容を更新しても更新内容は反映されません。

そこで編集内容が反映されるよう、以下のようにbroadcast_replace_tobroadcast_update_toに置き換えます。

class AiRequestJob < ApplicationJob
  queue_as :default

  def perform(ai_request_params, api_key)
    uuid = ai_request_params[:uuid]
    generated_idea = ai_request_params[:prompt]

    Turbo::StreamsChannel.broadcast_update_to("channel_#{uuid}",
      target: 'ai_output',
      partial: 'ai/output',
      locals: { generated_idea:  }
    )
  end
end

動作確認3

入力内容が更新されることを確認します。

Image from Gyazo

AiRequestJobの修正

最後に以下のようにFaraday Gemを使用して、OpenAI APIのリクエストを行います。

class AiRequestJob < ApplicationJob
  queue_as :default

  def perform(ai_request_params, api_key)
    connection = Faraday.new(url: 'https://api.openai.com')

    response = connection.post do |req|
      req.url "/v1/engines/#{ai_request_params[:ai_model]}/completions"
      req.headers['Content-Type'] = 'application/json'
      req.headers['Authorization'] = "Bearer #{api_key}"
      req.body = {
        prompt: ai_request_params[:prompt],
        max_tokens: 250,
        temperature: 0.5,
        n: 1
      }.to_json
    end

    json_response = JSON.parse(response.body)
    generated_idea = json_response['choices'][0]['text']

    uuid = ai_request_params[:uuid]
    Turbo::StreamsChannel.broadcast_update_to("channel_#{uuid}",
                                              target: 'ai_output',
                                              partial: 'ai/output',
                                              locals: { generated_idea: })
  end
end

ここではOpenAI APIへのリクエストを行い、生成されたテキストを取得し、Turbo Streamsを使用してクライアントにデータを非同期に送信しています。

  • connection = Faraday.new(url: 'https://api.openai.com')
    Faraday Gemを使用してOpenAi APIへの接続を確立しています。
  • response = connection.post do |req| ... end
    Faradayを使用してPOSTリクエストを送信しています。APIのエンドポイント、ヘッダー、ボディなどをブロック内で設定しています。
  • json_response = JSON.parse(response.body)
    APIから受け取ったレスポンスボディをJSON形式からパースして、json_response変数に格納しています。
  • generated_idea = json_response['choices'][0]['text']
    json_responseから生成されたテキストを取得しています。APIの応答データの中にあるchoices配列から、最初の要素のtextフィールドを抽出しています。
OpenAI Logo
コード不要でOpenAIを試すことができるPlaygroundを触ってみたコード不要でOpenAIを試すことができるPlaygroundを触ってみたので紹介しています。どのようなオプションがあるか、どのような出力結果になるかを手軽に試すことができるので、実際にコードを書く前の確認に良さそうです。...

動作確認4(画像のみ)

Davinci応答

今回の質問内容でベンチマークを計測すると、約8秒かかってしまうため、実際に開発で用いる際は何らかの対策が必要になりそうです。

class AiRequestJob < ApplicationJob
  queue_as :default

  def perform(ai_request_params, api_key)
    result = Benchmark.measure do
      connection = Faraday.new(url: 'https://api.openai.com')

      response = connection.post do |req|
        req.url "/v1/engines/#{ai_request_params[:ai_model]}/completions"
        req.headers['Content-Type'] = 'application/json'
        req.headers['Authorization'] = "Bearer #{api_key}"
        req.body = {
          prompt: ai_request_params[:prompt],
          max_tokens: 250,
          temperature: 0.5,
          n: 1
        }.to_json
      end

      json_response = JSON.parse(response.body)
      generated_idea = json_response['choices'][0]['text']

      uuid = ai_request_params[:uuid]
      Turbo::StreamsChannel.broadcast_update_to("channel_#{uuid}",
                                                target: 'ai_output',
                                                partial: 'ai/output',
                                                locals: { generated_idea: })
    end
    result.real # ベンチマーク計測
  end
end

まとめ

  • Fine-tuningモデルは、事前に大規模なデータセットで訓練された一般的な言語モデルを、特定のタスクやドメインに合わせて追加の訓練を行い、特定のタスクに特化したモデルを作成する手法のこと
  • 利用料金はモデルによって変わる
  • 回答結果を表示するには約8秒かかる(今回の場合)

参考

 

今回はOpenAIのAPIを用いて、Fine Tuningモデルを自由に選択できる自動応答機能を作ってみたので紹介しました。パラメータを色々いじって、どのような回答が得られるかを見てみても面白そうですね。