DIVX テックブログ

catch-img

ruby-openaiでAIの力を体感!ソースコード解説&簡単ハンズオン


目次[非表示]

  1. 1.はじめに
  2. 2.ruby-openaiとは?
    1. 2.1.ruby-openaiの機能一覧
    2. 2.2.ruby-openai自体の説明
  3. 3.ハンズオン
    1. 3.1.準備
    2. 3.2.ChatGPT
    3. 3.3.Embeddings
    4. 3.4.Files
    5. 3.5.Finetune
    6. 3.6.Image
    7. 3.7.Edit
    8. 3.8.Whisper
    9. 3.9.Transcribe
  4. 4.感想・まとめ
  5. 5.おわりに

こちらの記事はDIVXアドベントカレンダー2023の6日目の記事です。

はじめに

株式会社divxの細野・大石です。
現在、私たちの所属する株式会社divxにおいて、Rubyを使用した案件比率は少なくありません。
また、最近は「Chat-GPT」が巷を大変賑わせており、自然言語処理を活用する案件も今後一層増えてくるのではと感じています。
一方で、Rubyで自然言語処理モデルを使用するためのライブラリである、「ruby-openai」に関する日本語の資料は多くありません。
そこで、この記事ではRubyで自然言語処理モデルを扱うためのライブラリruby-openaiについて、ソースコードの解説 & ハンズオン形式の簡単な使用例を紹介しています。
最後まで目を通していただけますと幸いです。
尚、2023年5月時点のruby-openaiを参照しています。

ruby-openaiとは?

ruby-openaiとは、OpenAIの機能をプログラミング言語のRubyで利用するためのライブラリです。
このライブラリは、OpenAIの自然言語処理モデルにアクセスするための方法を、誰でも利用できる簡単な形で提供しています。
つまり、ruby-openaiを使用することで、高度な自然言語処理を簡単に実現できるということです。
実現できる機能の具体例としては、文章の生成や要約・翻訳・テキスト分類・画像生成などが挙げられます。
以降では、「ruby-openaiはどんな機能を提供しているのか」や「実際の使用例(ハンズオン)」などを紹介します。

ruby-openaiの機能一覧

ruby-openaiで提供されている機能を簡単に紹介します。
詳細内容は、ハンズオンの各項目にて記載しています。

◎ ChatGPT
ChatGPTは、会話形式でテキストを生成するために使用できるモデルです。このモデルを使用すると、一連のメッセージに対する応答を生成できます。

◎ Streaming ChatGPT
Streaming ChatGPTは、APIからリアルタイムでストリーミングできるため、より高速で魅力的なユーザーエクスペリエンスを作成するために利用できます。
この機能は、会話形式でテキストを生成するChatGPTのより高速なバージョンです。APIからのリアルタイムストリーミングを可能にするため、ChatGPTよりも高速で、より滑らかな会話を実現することができます。
※ 本記事でStreaming ChatGPTのハンズオンは行いません。

◎ Completions
Completionsは、テキストを入力すると、指定されたコンテキストやパターンに一致するテキスト補完を生成するプロンプトです。

◎ Edits
Editsは、指示されたテキストとその変更方法に従って編集を行います。

◎ Embeddings
Embeddingsは、テキストをベクトル(1536 次元の数値配列)に変換します。
このベクトル同士の距離を計算することで、テキストの類似性を測定することができます。

◎ Files
Filesは、最大1GBのJSONファイルのアップロードや、アップロードしたJSONファイルに対する処理を行うことができます。

◎ Finetune
GPT-3のモデル(davinci・curie・babbage・ada)をカスタマイズします。

◎ Image
画像生成や画像編集など、画像操作を行います。

◎ Moderation
コンテンツが OpenAIの使用ポリシーに準拠しているかどうかを確認できます。

◎ Whisper
Whisperとは、オーディオファイルをテキストへ変換することができる音声認識モデルです。
Whisperを使用することで、音声ファイルからの文字起こしなどが行えます。

ruby-openai自体の説明

まず、ruby-openaiのソースコードを見ていきましょう。
リポジトリは以下になります。

https://github.com/alexrudall/ruby-openai

mainファイルは、lib/openai.rbになります。

■ lib/openai.rb

require "faraday"
require "faraday/multipart"
require_relative "openai/http"
require_relative "openai/client"
require_relative "openai/files"
require_relative "openai/finetunes"
require_relative "openai/images"
require_relative "openai/models"
require_relative "openai/version"
module OpenAI
  class Error < StandardError; end
  class ConfigurationError < Error; end
  class Configuration
    attr_writer :access_token
    attr_accessor :api_version, :organization_id, :uri_base, :request_timeout
    DEFAULT_API_VERSION = "v1".freeze
    DEFAULT_URI_BASE = "https://api.openai.com/".freeze
    DEFAULT_REQUEST_TIMEOUT = 120
    def initialize
      @access_token = nil
      @api_version = DEFAULT_API_VERSION
      @organization_id = nil
      @uri_base = DEFAULT_URI_BASE
      @request_timeout = DEFAULT_REQUEST_TIMEOUT
    end
    def access_token
      return @access_token if @access_token
      error_text = "OpenAI access token missing! See https://github.com/alexrudall/ruby-openai#usage"
      raise ConfigurationError, error_text
    end
  end
  class << self
    attr_writer :configuration
  end
  def self.configuration
    @configuration ||= OpenAI::Configuration.new
  end
  def self.configure
    yield(configuration)
  end
end

行っていることは、大きく以下の2点です。

  • requireでgemを、require_relativeでopanaiディレクトリ配下のモジュールを読み込み
  • その他バージョンやリクエストタイムなどを設定

続いて、require_relativeで読み込んでいる各モジュールの説明です。
まずは、lib/openai/http.rbです。

■ lib/openai/http.rb

module OpenAI
  module HTTP
    def get(path:)
      to_json(conn.get(uri(path: path)) do |req|
        req.headers = headers
      end&amp;.body)
    end
    def json_post(path:, parameters:)
      to_json(conn.post(uri(path: path)) do |req|
        if parameters[:stream].respond_to?(:call)
          req.options.on_data = to_json_stream(user_proc: parameters[:stream])
          parameters[:stream] = true # Necessary to tell OpenAI to stream.
        elsif parameters[:stream]
          raise ArgumentError, "The stream parameter must be a Proc or have a #call method"
        end
        req.headers = headers
        req.body = parameters.to_json
      end&amp;.body)
    end
    def multipart_post(path:, parameters: nil)
      to_json(conn(multipart: true).post(uri(path: path)) do |req|
        req.headers = headers.merge({ "Content-Type" => "multipart/form-data" })
        req.body = multipart_parameters(parameters)
      end&amp;.body)
    end
    def delete(path:)
      to_json(conn.delete(uri(path: path)) do |req|
        req.headers = headers
      end&amp;.body)
    end
    private
    # 省略
  end
end

publicメソッドとして、getjson_postmultipart_postdeleteメソッドを定義しています。
json_postmultipart_postの違いは、Content-Typeにmultipart/form-dataを指定しているか否かです。Content-Typeにmultipart/form-dataを指定するmultipart_postは、1つのHTTPリクエストに複数の異なる種類のデータを含める際に使用されます。これは、非テキストデータである音声データや画像データを取り扱う際にも使用されます。
次に、lib/openai/client.rbです。

■ lib/openai/client.rb

module OpenAI
  class Client
    extend OpenAI::HTTP
    def initialize(access_token: nil, organization_id: nil, uri_base: nil, request_timeout: nil)
      OpenAI.configuration.access_token = access_token if access_token
      OpenAI.configuration.organization_id = organization_id if organization_id
      OpenAI.configuration.uri_base = uri_base if uri_base
      OpenAI.configuration.request_timeout = request_timeout if request_timeout
    end
    def chat(parameters: {})
      OpenAI::Client.json_post(path: "/chat/completions", parameters: parameters)
    end
    def completions(parameters: {})
      OpenAI::Client.json_post(path: "/completions", parameters: parameters)
    end
    def edits(parameters: {})
      OpenAI::Client.json_post(path: "/edits", parameters: parameters)
    end
    def embeddings(parameters: {})
      OpenAI::Client.json_post(path: "/embeddings", parameters: parameters)
    end
    def files
      @files ||= OpenAI::Files.new
    end
    def finetunes
      @finetunes ||= OpenAI::Finetunes.new
    end
    def images
      @images ||= OpenAI::Images.new
    end
    def models
      @models ||= OpenAI::Models.new
    end
    def moderations(parameters: {})
      OpenAI::Client.json_post(path: "/moderations", parameters: parameters)
    end
    def transcribe(parameters: {})
      OpenAI::Client.multipart_post(path: "/audio/transcriptions", parameters: parameters)
    end
    def translate(parameters: {})
      OpenAI::Client.multipart_post(path: "/audio/translations", parameters: parameters)
    end
  end
end

行っていることは以下の2点です。

  • lib/openai/http.rbjson_postまたはmultipart_postメソッドへ、必要な「path」と「parameters」を渡しています。
    lib/openai/http.rbjson_postまたはmultipart_postメソッドでは、「Faraday」というgemを用いてOpenAIのエンドポイントへリクエストを投げています。
    例えば、OpenAIのChatGPTの機能を使用したい場合、POSThttps://api.openai.com/v1/chat/completionsというエンドポイントへリクエストを投げることで機能を使用できます。

    https://platform.openai.com/docs/api-reference/chat

  • OpenAIのFilesFinetunesImagesModelsそれぞれのクラスのインスタンス生成を行い、それぞれのクラスのメソッドを使える状態にしています。

続いて、lib/openai/files.rbです。

■ lib/openai/files.rb

module OpenAI
  class Files
    def initialize(access_token: nil, organization_id: nil)
      OpenAI.configuration.access_token = access_token if access_token
      OpenAI.configuration.organization_id = organization_id if organization_id
    end
    def list
      OpenAI::Client.get(path: "/files")
    end
    def upload(parameters: {})
      validate(file: parameters[:file])
      OpenAI::Client.multipart_post(
        path: "/files",
        parameters: parameters.merge(file: File.open(parameters[:file]))
      )
    end
    def retrieve(id:)
      OpenAI::Client.get(path: "/files/#{id}")
    end
    def content(id:)
      OpenAI::Client.get(path: "/files/#{id}/content")
    end
    def delete(id:)
      OpenAI::Client.delete(path: "/files/#{id}")
    end
    private
    # 省略
  end
end

各メソッド、lib/openai/client.rbgetmultipart_postメソッドに必要な引数を渡しています。
その結果、OpenAIのエンドポイントへリクエストが投げられます。
以降、同様の説明が続きます。

続いて、lib/openai/finetunes.rbです。

■ lib/openai/finetunes.rb

module OpenAI
  class Finetunes
    def initialize(access_token: nil, organization_id: nil)
      OpenAI.configuration.access_token = access_token if access_token
      OpenAI.configuration.organization_id = organization_id if organization_id
    end
    def list
      OpenAI::Client.get(path: "/fine-tunes")
    end
    def create(parameters: {})
      OpenAI::Client.json_post(path: "/fine-tunes", parameters: parameters)
    end
    def retrieve(id:)
      OpenAI::Client.get(path: "/fine-tunes/#{id}")
    end
    def cancel(id:)
      OpenAI::Client.multipart_post(path: "/fine-tunes/#{id}/cancel")
    end
    def events(id:)
      OpenAI::Client.get(path: "/fine-tunes/#{id}/events")
    end
    def delete(fine_tuned_model:)
      if fine_tuned_model.start_with?("ft-")
        raise ArgumentError, "Please give a fine_tuned_model name, not a fine-tune ID"
      end
      OpenAI::Client.delete(path: "/models/#{fine_tuned_model}")
    end
  end
end

各メソッド、lib/openai/client.rbgetmultipart_postメソッドに必要な引数を渡しています。
その結果、OpenAIのエンドポイントへリクエストが投げられます。
続いて、lib/openai/images.rbです。

■ lib/openai/images.rb

module OpenAI
  class Images
    def initialize(access_token: nil, organization_id: nil)
      OpenAI.configuration.access_token = access_token if access_token
      OpenAI.configuration.organization_id = organization_id if organization_id
    end
    def generate(parameters: {})
      OpenAI::Client.json_post(path: "/images/generations", parameters: parameters)
    end
    def edit(parameters: {})
      OpenAI::Client.multipart_post(path: "/images/edits", parameters: open_files(parameters))
    end
    def variations(parameters: {})
      OpenAI::Client.multipart_post(path: "/images/variations", parameters: open_files(parameters))
    end
    private
    # 省略    
  end
end

各メソッド、lib/openai/client.rbgetmultipart_postメソッドに必要な引数を渡しています。
その結果、OpenAIのエンドポイントへリクエストが投げられます。
続いて、lib/openai/models.rbです。

■ lib/openai/models.rb

module OpenAI
  class Models
    def initialize(access_token: nil, organization_id: nil)
      OpenAI.configuration.access_token = access_token if access_token
      OpenAI.configuration.organization_id = organization_id if organization_id
    end
    def list
      OpenAI::Client.get(path: "/models")
    end
    def retrieve(id:)
      OpenAI::Client.get(path: "/models/#{id}")
    end
  end
end

各メソッド、lib/openai/client.rbgetmultipart_postメソッドに必要な引数を渡しています。
その結果、OpenAIのエンドポイントへリクエストが投げられます。
最後に、lib/openai/version.rbです。

■ lib/openai/version.rb

module OpenAI
  VERSION = "4.1.0".freeze
end


バージョン情報の定数を定義しています。

ハンズオン

ここからは、ruby-openaiで使用出来る各機能を実行する、簡単なハンズオンを紹介します。

◎ リポジトリ
以下はハンズオンの完成形です。必要に応じてご覧ください。

https://github.com/Naoki-014/playing-with-ruby-openai

◎ OpenAIの価格表
OpenAIのAPIを使用する場合、一定量以上使用すると料金が発生しますのでご注意ください。

https://openai.com/pricing

そのため、適度に請求ページを見ながらハンズオンを進めることをお勧めいたします。

https://platform.openai.com/account/usage

これからOpen AIのアカウントを作成する方や、OpenAIアカウントの作成から3ヶ月以内の方であれば、5ドル(2023年6月時点)分のクレジットが付与される為、ハンズオンで料金が発生する可能性は少ないと思われます。

準備

ハンズオンのための事前準備です。
※ 以降はrubyの環境構築が完了している前提です。
まず、OpenAIのアカウント作成後、下記リンクよりAPI KEYを取得しましょう。

https://platform.openai.com/account/api-keys

次に、取得したAPI KEYをOPENAI_ACCESS_TOKENという変数名で環境変数に設定します。

続いて、以下のリポジトリをgit cloneします。

https://github.com/Naoki-014/playing-with-ruby-openai

git cloneしたディレクトリへ移動し、bundle installを実行しましょう。
これでハンズオンの準備は完了です。

最後に、git cloneしたリポジトリのsample_cli.rbには既に3つのメソッドが記載されているため、簡単に説明します。

◎ clientメソッド
OpenAI::Clientクラスのインスタンスを生成するメソッドです。

◎ model_versionメソッド
渡された引数を、変数model_versionに代入するメソッドです。
引数が渡されなかった場合は、gpt-3.5-turboというモデルを代入します。

◎ gets_chompメソッド
ユーザーから入力された文字列を受け取り、末尾の改行文字を取り除いた文字列を返すメソッドです。

それでは、ハンズオンを始めましょう。

ChatGPT

ChatGPTは、会話形式でテキストを生成するために使用できるモデルです。このモデルを使用する

ChatGPTは、会話形式でテキストを生成するために使用できるモデルです。このモデルを使用すると、一連のメッセージに対する応答を生成できます。
また、Chat-GPTは自然な表現を生成するために学習されており、会話の流れを追うような応答を返すことができます。そのため、顧客サポートやチャットボットの開発に役立ちます。

実装コード

今回は、「プロンプトを入力すると応答が返ってくる機能」を実装します。
まずは、シンプルな応答を返しましょう。

■ sample_cli.rb

desc "chat", "ChatGPT API"
def chat
  puts "Please input your message."
  chat_gpt = RubyOpenAI::ChatGPT.new(client, model_version("gpt-3.5-turbo"))
  input = gets_chomp
  response = chat_gpt.get_response(messages: [{ role: "user", content: input }])
  puts response["content"]
end

■ ruby_openai/chat_gpt.rb

module RubyOpenAI
  class ChatGPT
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
    # optionsに指定しているtemperatureは、返答のランダム具合を調整するオプションとなっています
    # 0.8など1に近い値を指定した場合は、返答のランダム性が高く、0.2など低い値を指定した場合は、返答内容が安定します
    def get_response(required_params, options = { temperature: 0.7 })
      response = client.chat(
        parameters: add_parameters(required_params, options)
      )
      response.dig("choices", 0, "message")
    end
    private
    def add_parameters(required_params, options)
      parameters = {
        model: self.model,
        messages: required_params[:messages],
        temperature: options[:temperature]
      }
    end
  end
end

解説
今回実装したコードの解説です。

  1. OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::ChatGPTクラスのインスタンスを生成
        - 第二引数に指定した「gpt-3.5-turbo」は、AIの言語モデルの種類の1つであり、今回は、gpt-3.5の中で最も高性能と言われる言語モデルを指定しました。2023年6月時点では、「gpt-4」が最新の言語モデルとなっておりますが、「gpt-4」を使用するためには、Open AIのAPI waitlistに登録する必要があるなどの制約もあったため、今回は「gpt-3.5-turbo」を指定しました。もし「gpt-4」を使用したい場合は、以下の記事などを参考にすると良いかもしれません。

    https://note.com/otaka_ai/n/na1e7af959963

  2. ユーザーが入力した「プロンプト」を引数に、RubyOpenAI::ChatGPTクラスのget_responseメソッドを呼び出す
  3. RubyOpenAI::ChatGPTクラスのget_responseメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う

    ■ 例
    response = client.chat(

       parameters: {
         # modelには、AIの言語モデルの種類を指定
         model: "gpt-3.5-turbo"
         # roleにはsystem・user・assistantのいずれかを、contentにはメッセージを指定
         messages: [{ role: "user", content: "Hello!"}],
         temperature: 0.7,
       }
     )
     puts response.dig("choices", 0, "message", "content")
     # => "Hello! How may I assist you today?"

  4. レスポンスをターミナルへ出力

挙動確認
ruby sample_cli.rb chatというコマンドを入力しましょう。
この時点では、以下のような挙動になります。


次に、繰り返し聞けるよう実装を追加します。
実装コード
■ sample_cli.rb

desc "chat", "ChatGPT API"
def chat
  chat_gpt = RubyOpenAI::ChatGPT.new(client, model_version)
  input = ""
  # while文を使用し、ユーザーがexitと入力するまでは、チャットが継続できるように変更
  while input != "exit" do
    puts "Please input your message."
    puts "If you want to exit, please input 'exit'."
    input = gets_chomp
    return if input == "exit"
    response = chat_gpt.get_response(messages: [{ role: "user", content: input }])
    puts response["content"]
  end
end

解説
今回実装したコードの解説です。
変更内容としては以下のみとなります。

  • while文を使用し、ユーザーがexitと入力するまでは、チャットが継続できるように変更

最初のコードでは1回のやり取りしか行えませんでしたが、これで繰り返し質問が投げられるようになります。
挙動確認
ruby sample_cli.rb chatというコマンドを入力しましょう。
この時点では、以下のような挙動になります。

しかし、実はこの時点では前回の質問内容を引き継ぐことが出来ておらず、「しりとり」のように前回の会話に続けた応答を得ることが出来ません。
最後にしりとりもできるように、以下のように実装を追加します。
実装コード
■ sample_cli.rb

desc "chat", "ChatGPT API"
def chat
  messages = []
  chat_gpt = RubyOpenAI::ChatGPT.new(client, model_version)
  input = ""
  while input != "exit" do
    puts "Please input your message."
    puts "If you want to exit, please input 'exit'."
    input = gets_chomp
    return if input == "exit"
    messages.push({ role: "user", content: input })
    begin
      response = chat_gpt.get_response(messages: messages)
      puts response["content"]
    rescue
      puts "Sorry, An unexpected error has occurred."
      puts "Please try again after 20sec."
    end
  end
end

解説
今回実装したコードの解説です。

前回の質問内容が引き継がれるように、messagesというArray型の変数に、過去の質問内容と一緒にOpenAIにリクエストを送信するように変更

レスポンスが取得出来なかった場合の例外処理を追加
    - Open AIでは、こちらの条件を元に登録情報や使用する言語モデルによって質問に制限が入るようです。しりとりのように短時間に連続で質問を重ねた場合には以下のようなエラーが発生することを確認したため、例外処理を追加しました。
    - Rate limit reached for default-gpt-3.5-turbo in organization org-xxxxxxx on requests per min. Limit: 3 / min. Please try again in 20s.

挙動確認

ruby sample_cli.rb chatというコマンドを入力しましょう。
最終的には、以下のような挙動になります。

途中で例外が発生したものの、時間を空けて入力すれば引き続きしりとりを継続できる(しりとりのルールは教えなければならない模様)


これで、しりとりが出来るようになりましたね。

Completions

Completionsは、テキストを入力すると、指定されたコンテキストやパターンに一致するテキスト補完を生成するプロンプトです。
例えば「Rubyとはなん」というテキストを渡すと、「Rubyとはなんですか」とテキストを補完します。
今回は「入力したテキストを補完する機能」を実装します。
実装コード
■ sample_cli.rb

desc "completion", "Completion API"
def completion
  puts "Please input your message."
  completion = RubyOpenAI::Completion.new(client, model_version("text-davinci-001"))
  input = gets_chomp
  response = completion.get_response(prompt: input)
  puts input + response.join("")
end

■ ruby_openai/completion.rb

module RubyOpenAI
  class Completion
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def get_response(required_params, options = { max_tokens: 5 })
      response = client.completions(
        parameters: add_parameters(required_params, options)
      )["choices"].map { |c| c["text"] }
    end
    private
    def add_parameters(required_params, options)
      parameters = {
        model: self.model,
        prompt: required_params[:prompt],
        max_tokens: options[:max_tokens]
      }
    end
  end  
end

解説
今回実装したコードの解説です。

OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::Completionクラスのインスタンスを生成

ユーザーが入力した「テキスト」を引数に、RubyOpenAI::Completionクラスのget_responseメソッドを呼び出す

RubyOpenAI::Completionクラスのget_responseメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
 ■ 例
 response = client.completions(
   parameters: {
     model: "text-davinci-001", # 言語モデルの種類
     prompt: "Once upon a time", # 補完したいテキスト
     max_tokens: 5 # tokenの使用制限
   }
 )
 puts response["choices"].map { |c| c["text"] }
 # => [", there lived a great"]

レスポンスをターミナルへ出力
挙動確認
ruby sample_cli.rb completionというコマンドを入力しましょう。
以下のような挙動になります。

文字が補完される

適切な文字が補完されました。

Edits

Editsは、指示されたテキストとその変更方法に従って編集を行います。
例えば、スペルミスの修正や敬語への変換などを指示することで、テキストを自動修正することができます。
今回は「入力したテキストを敬語へ修正する機能」を実装します。
実装コード
■ sample_cli.rb

desc "edit", "Edit API"
def edit
  puts "Please input your message."
  edit = RubyOpenAI::Edit.new(client, model_version("text-davinci-edit-001"))
  input = gets_chomp
  puts "Please input your instruction."
  instruction = gets_chomp
  response = edit.get_response(input: input, instruction: instruction)
  puts response
end

■ ruby_openai/edit.rb

module RubyOpenAI
  class Edit
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def get_response(required_params, options = {})
      response = client.edits(
        parameters: add_parameters(required_params, options)
      ).dig("choices", 0, "text")
      response
    end
    private
    def add_parameters(required_params, options)
      parameters = {
        model: self.model,
        input: required_params[:input],
        instruction: required_params[:instruction]
      }
    end
  end
end


解説
今回実装したコードの解説です。

OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::Editクラスのインスタンスを生成

ユーザーが入力した「テキスト」と「編集指示」を引数に、RubyOpenAI::Editクラスのget_responseメソッドを呼び出す

RubyOpenAI::Editクラスのget_responseメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
■ 例
 response = client.edits(
   parameters: {
     model: "text-davinci-edit-001",
     input: "What day of the wek is it?",
     instruction: "Fix the spelling mistakes"
   }
 )
 puts response.dig("choices", 0, "text")
 # => What day of the week is it?

レスポンスをターミナルへ出力
挙動確認
ruby sample_cli.rb editというコマンドを入力しましょう。

テキストを敬語に変換した

指示通りにテキストが敬語へ変換されました。

Embeddings

Embeddingsは、テキストをベクトル(1536 次元の数値配列)に変換します。
このベクトル同士の距離を計算することで、テキストの類似性を測定することができます。
テキストの類似性は、レスポンスの値が1.0に近い程類似性が高く、その逆は類似性が低いことを意味します。
今回は「入力した2つのテキストの類似性を測定する機能」を実装します。
実装コード
■ sample_cli.rb

desc "embedding", "Embedding API"
def embedding
  embedding = RubyOpenAI::Embedding.new(client, model_version("text-embedding-ada-002"))
  puts "Please input your message."
  response_1= embedding.get_response(input: gets_chomp)
  vector1 = Vector.elements(response_1)
  puts "Please input your message to compare."
  response_2 = embedding.get_response(input: gets_chomp)
  vector2 = Vector.elements(response_2)
    
  # ベクトル同士の内積を計算(計算結果の値が1.0に近いほど類似性が高い)
  calc_result = vector2.inner_product(vector1)/(vector1.norm() * vector2.norm())
  puts calc_result
end

■ ruby_openai/embedding.rb

module RubyOpenAI
  class Embedding
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def get_response(required_params, options = {})
      response = client.embeddings(
        parameters: add_parameters(required_params, options)
      ).dig("data", 0, "embedding")
    end
    private
    def add_parameters(required_params, options)
      parameters = {
        model: self.model,
        input: required_params[:input]
      }
    end
  end
end

解説
今回実装したコードの解説です。

OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::Embeddingクラスのインスタンスを生成

ユーザーが入力した「類似性測定の対象となるテキスト2つ」を引数に、RubyOpenAI::Embeddingクラスのget_responseメソッドを呼び出す

RubyOpenAI::Embeddingクラスのget_responseメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
■ 例
response = client.embeddings(
   parameters: {
     model: "babbage-similarity",
     input: "The food was delicious and the waiter..."
   }
 )
 puts response.dig("data", 0, "embedding")
 # => Vector representation of your embedding

レスポンスをターミナルへ出力

挙動確認
ruby sample_cli.rb embeddingというコマンドを入力しましょう。

リンゴとバナナの類似性を検証(1.0に近い程、類似性が高い)

リンゴと人間の類似性を検証(1.0に近い程、類似性が高い)

「リンゴとバナナ」「リンゴと人間」の類似性を比較すると、前者の類似性の方が高い値となりました。

他にもいくつかのパターンを検証してみましたが0.8以下の値は滅多に返却されず、個人的には「0.9を越えると類似性が高くそれ以下の場合は類似性が低い」という印象でした。

Files

Filesは、最大1GBのJSONファイルのアップロードや、アップロードしたJSONファイルに対する処理を行うことができます。
処理としては「ファイルのリストを取得」「単体ファイルの取得」「単体ファイルの内容を取得」「単体ファイルの削除」を行うことができます。
実際の使用例としては、企業が蓄積した膨大なデータの中から、特定のキーワードを含むデータを抽出する場合などが挙げられます。
ruby-openaiでは、以下のメソッドを用意しています。

◎ Upload
指定したファイルをアップロードします。

◎ List
アップロード済みのファイルを一覧で取得します。

◎ Retrieve
アップロードしたファイルのIDを指定し、該当ファイルを取得します。

◎ Content
アップロードしたファイルのIDを指定し、該当ファイルの内容します。
注意点として、フリープランではContentメソッドを利用できません。
「To help mitigate abuse, downloading of fine-tune training files is disabled for free accounts.」というエラーメッセージ

◎ Delete
アップロードしたファイルのIDを指定し、該当ファイルを削除します。
今回は、「JSONファイルをアップロードし操作する機能」を、以下2つの工程に分けて実装します。

  1. JSONファイルをアップロードする機能
  2. アップロードしたJSONファイルを操作する機能

まずは、「JSONファイルをアップロードする機能」を実装します。
実装コード
■ sample_cli.rb

desc "file", "File API"
def file
  client = OpenAI::Client.new
  file = RubyOpenAI::File.new(client, model_version("ada"))
  puts "Please input the path to the json file."
  response = file.get_response(file: gets_chomp, purpose: "fine-tune")
  puts response
end
■ ruby_openai/file.rb
module RubyOpenAI
  class File
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def get_response(required_params, options = {})
      response = client.files.upload(
        parameters: add_parameters(required_params, options)
      )
      response
    end
    private
    def add_parameters(required_params, options)
      parameters = {
        file: required_params[:file],
        purpose: required_params[:purpose]
      }
    end
  end
end

最後に、git cloneしたディレクトリのルートディレクトリ配下にtest.jsonなどのファイルを作成し、以下の内容を貼り付けましょう。

{"prompt":"Overjoyed with my new phone! ->", "completion":" positive"}


test.jsonを作成

解説
今回実装したコードの解説です。

OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::Fileクラスのインスタンスを生成

ユーザーが入力した「ファイルデータ(jsonlファイル)」を引数に、RubyOpenAI::Fileクラスのget_responseメソッドを呼び出す

RubyOpenAI::Fileクラスのget_responseメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
■ 例

 response = client.files.upload(
   parameters: { 
     file: "path/to/sentiment.jsonl",
     purpose: "fine-tune" 
   }
 )

レスポンスをターミナルへ出力

挙動確認
ruby sample_cli.rb fileというコマンドを入力しましょう。
以下のような挙動になります。


ファイルをアップロードできた

次に、「アップロードしたJSONファイルを操作する機能」を実装します。

実装コード
■ sample_cli.rb

desc "file", "File API"
options :upload => :boolean, :list => :boolean, :retrieve => :boolean, :delete => :boolean
def file
  client = OpenAI::Client.new
  file = RubyOpenAI::File.new(client, model_version("ada"))
  case options.keys.join("")
  when "upload"
    puts "Please input the path to the json file."
    response = file.get_response(file: gets_chomp, purpose: "fine-tune")
    puts response
  when "list"
    puts file.list
  when "retrieve"
    puts "Please input file id."
    puts file.retrieve(gets_chomp)
  when "delete"
    puts "Please input file id."
    puts file.delete(gets_chomp)
  end
end

■ ruby_openai/file.rb

module RubyOpenAI
  class File
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def get_response(required_params, options = {})
      response = client.files.upload(
        parameters: add_parameters(required_params, options)
      )
      response
    end
    def list
      response = client.files.list
    end
    def retrieve(file_id)
      client.files.retrieve(id: file_id)
    end
    def delete(file_id)
      client.files.delete(id: file_id)
    end
    private
    def add_parameters(required_params, options)
      parameters = {
        file: required_params[:file],
        purpose: required_params[:purpose]
      }
    end
  end
end

解説
今回実装したコードの解説です。
まずは、listオプションを指定したコマンドの場合です。

  1. ruby sample_cli.rb file —オプションのコマンドで実行したいため、thorの機能であるoptionsを使用
        - これにより、ファイル操作を行いたい場合はruby sample_cli.rb image —listなどのコマンドで実行することができます。
  2. OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::Fileクラスのインスタンスを生成
  3. ユーザーが入力した「ファイルID(アップロードした際に出力されるID情報)」を引数に、RubyOpenAI::Fileクラスのlistメソッドを呼び出す
  4. レスポンスをターミナルへ出力

次に、retrieveオプションを指定したコマンドの場合です。

  1. OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::Fileクラスのインスタンスを生成
  2. ユーザーが入力したファイルID(アップロードした際に出力されるID情報)を引数に、RubyOpenAI::Fileクラスのretrieveメソッドを呼び出す
  3. レスポンスをターミナルへ出力

最後に、deleteオプションを指定したコマンドの場合です。

  1. OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::Fileクラスのインスタンスを生成
  2. ユーザーが入力したファイルID(アップロードした際に出力されるID情報)を引数に、RubyOpenAI::Fileクラスのdeleteメソッドを呼び出す
  3. レスポンスをターミナルへ出力

挙動確認
まずは、ruby sample_cli.rb file --listというコマンドを入力しましょう。
アップロード済みのファイルを一覧で取得できます。

配列に先ほどアップロードしたファイルも含まれている


次に、ruby sample_cli.rb file --retrieveというコマンドを入力しましょう。
指定したIDのファイルを取得できます。

先ほどアップロードしたファイルを取得できる


最後に、ruby sample_cli.rb file --deleteというコマンドを入力しましょう。
指定したIDのファイルを削除できます。

指定したIDのファイルを削除できた


Finetune

前提としてFineTuningとは、既存のAIモデルを追加のデータで再調整する手法です。
元々広範なデータで事前トレーニングされたモデルを、特定のタスクに適応させるために使用されます。
これにより、より高品質な結果や多様なタスクへの適用が可能になります。
FineTuningを行うと、アプリケーションに合わせてGPT-3のモデル(davinci・curie・babbage・ada)をカスタマイズして利用できるようになります。
FineTuningすることによって見込めるメリットとしては、以下が挙げられます。

  • プロンプトデザインよりも高品質な結果が得られる

  • プロンプトに収まりきらないほどの多くの例に対してトレーニングが可能
  • 短いプロンプトによるトークンの節約
  • 低レイテンシのリクエスト

やや分かりづらい内容なため、今回のゴールを先に示します。
現在、「宇宙兄弟 南波六太の誕生日はいつですか?」という質問をChatGPTへ投げかけると、以下のような返答となります。

誕生日は不明と返ってくる


しかし、南波六太の誕生日は「1993年10月28日」だと作者の小山宙哉さんより明らかになっています。
そこで、モデルを学習し正しい南波六太の誕生日を答えられるようにします。
ruby-openaiでは、以下のメソッドを用意しています。
◎ Create
前提準備として、ファインチューニングで使用するトレーニングデータ(jsonlファイル)をアップロードし、ファイルIDを取得しておく必要があります(Fileの項目参照)。
トレーニングデータは以下のような内容になります。

{"prompt":"プロンプト", "completion":"期待する返答"}

注意点として、ファインチューニングを行うためのデータは、少なくとも200個の例を用意することが推奨されています。
データが少ない場合、期待通りのカスタマイズとはならない可能性が高いようです。
Createメソッドは、アップロードしたファイルIDを使用して、モデルをファインチューニングします。
つまり、トレーニングデータを用いてモデルのカスタマイズをするということです。
レスポンスとしては、ファインチューニングIDを取得できます。
ファインチューニング済みのモデルが出来たかどうかは、後述のListまたはRetrieveメソッドでファインチューニングIDを指定することで分かりますが、しばらく待つ場合があります。
◎ Cancel
指定したファインチューニングIDの、ファインチューニング処理をキャンセルします。
◎ List
ファインチューニングの処理に入ったモデルを一覧で取得します。
◎ Retrieve
ファインチューニングの処理に入ったモデルを個別に取得します。
◎ Completions
ファインチューニング済みのモデルを使用できます。
◎ Delete
ファインチューニング済みのモデルを削除します。
ここからは以下2つの工程に分けて、カスタマイズされたモデルを操作する機能を実装します。

トレーニングデータを用意し、モデルをカスタマイズする機能

カスタマイズしたモデルを操作する機能
まずは、「トレーニングデータを用意し、モデルをカスタマイズする機能」です。
実装コード
■ sample_cli.rb

desc "finetune", "FineTune API"
  options :create => :boolean, :cancel => :boolean
  def finetune
    client = OpenAI::Client.new
    finetune = RubyOpenAI::FineTune.new(client, model_version("ada"))
    case options.keys.join("")
    when "create"
      puts "Please input the path to the json file for fine tuning."
      response = finetune.create(file: gets_chomp, purpose: "fine-tune")
      puts "created fine tune id : #{response}"
    when "cancel"
      puts "Please input fine tune id."
      response = finetune.cancel(gets_chomp)
      puts "cancelled fine tune id : #{response}"
    end
  end

■ ruby_openai/finetune.rb

module RubyOpenAI
  class FineTune
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
    def create(required_params, options = {})
      file = RubyOpenAI::File.new(client, model)
      file_id = file.get_response(required_params)["id"]
      response = client.finetunes.create(
        parameters: add_parameters(file_id, model)
      )
      response["id"]
    end
    def cancel(fine_tune_id)
      responce = client.finetunes.cancel(id: fine_tune_id)
      responce["id"]
    end
    private
    def add_parameters(file_id, options)
      parameters = {
        training_file: file_id,
        model: self.model
      }
    end
  end
end

最後に、git cloneしたディレクトリのルートディレクトリ配下にfinetune.jsonなどのトレーニング用ファイルを作成しましょう。
ファイルの中身には200個以上のデータが推奨されているため、私は下記内容を×100回以上記述しました。

{"prompt": "宇宙兄弟 南波六太の誕生日は?", "completion": "1993年10月28日生まれです。本人は「サッカーワールドカップ予選敗退の『ドーハの悲劇』の日生まれ」と言っています。"}
{"prompt": "宇宙兄弟 「南波六太」の読み方は?", "completion": "「なんば むった」です。あだ名は「むっくん」です。"}

※ 同じ記述を繰り返すのではなく、様々な内容のトレーニングデータを用意することが好ましいです。今回は便宜的に繰り返しの記述にしています。

200個以上のデータを用意

解説
今回実装したコードの解説です。
まずは、createオプションを指定したコマンドの場合です。
今回カスタマイズするモデルは特に拘りがなかったため、コストが安く高速な「ada」を採用しています。

ruby sample_cli.rb finetune —オプションのコマンドで実行したいため、thorの機能であるoptionsを使用
    - これにより、モデルのファインチューニングを行いたい場合はruby sample_cli.rb finetune —create、モデルのファインチューニングをキャンセル場合はruby sample_cli.rb image —cancelのコマンドで実行することができます。

ユーザーが入力した「トレーニングデータ(jsonファイル)」を引数に、RubyOpenAI::FineTuneクラスのcreateメソッドを呼び出す

RubyOpenAI::FineTuneクラスのcreateメソッド内で、RubyOpenAI::Fileクラスのget_responseメソッドを呼び出し、トレーニングデータのファイルIDを取得

RubyOpenAI::FineTuneクラスのcreateメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
 ■ 例

 response = client.finetunes.create(
   parameters: {
     training_file: file_id, # トレーニングデータのファイルID
     model: "ada" # モデル名
   }
 )
 fine_tune_id = response["id"]

  
レスポンスからファインチューンIDを取得し、ターミナルへ出力
つぎに、cancelオプションを指定したコマンドの場合です。

  • ユーザーが入力したファインチューンID(createで取得した値)を引数に、RubyOpenAI::FineTuneクラスのcancelメソッドを呼び出す
  • RubyOpenAI::FineTuneクラスのcancelメソッド内で、OpenAI::Clientクラスのcancelメソッドを呼び出す

レスポンスからキャンセルしたファインチューンIDを取得し、ターミナルへ出力
挙動確認
ruby sample_cli.rb finetune --createというコマンドを入力しましょう。
createオプションを指定した場合は、以下のように作成されたファインチューニングIDが返ります。

作成されたファインチューニングIDが返る

ruby sample_cli.rb finetune --cancelというコマンドを入力しましょう。
cancelオプションを指定した場合は、以下のようにキャンセルされたファインチューニングIDが返ります。

キャンセルされたファインチューニングIDが返る

実装コード
■ sample_cli.rb


  desc "finetune", "FineTune API"
  options :create => :boolean, :cancel => :boolean, :list => :boolean, :retrieve => :boolean, :completions => :boolean, :delete => :boolean
  def finetune
    client = OpenAI::Client.new
    finetune = RubyOpenAI::FineTune.new(client, model_version("ada"))
    case options.keys.join("")
    when "create"
      # 省略
    when "cancel"
      # 省略
    when "list"
      responses = finetune.list
      responses.each.with_index(1) do |response, index|
        puts "【#{index}】 fine tune id : #{response["id"]}, status : #{response["status"]}, fine tuned model : #{response["fine_tuned_model"]}"
      end
    when "retrieve"
      puts "Please input fine tune id."
      response = finetune.retrieve(gets_chomp)
      puts "fine tune id : #{response["id"]}, status : #{response["status"]}, fine tuned model : #{response["fine_tuned_model"]}"
    when "completions"
      puts "Please input fine tuned model."
      fine_tuned_model = gets_chomp
      puts "Please input your message."
      prompt = gets_chomp
      response = finetune.completions(fine_tuned_model: fine_tuned_model, prompt: prompt)
      puts response
    when "delete"
      puts "Please input fine tuned model."
      response = finetune.delete(gets_chomp)
      puts "deleted fine tune model : #{response}"
    end
  end

■ ruby_openai/finetune.rb

module RubyOpenAI
  class FineTune
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
    def create(required_params, options = {})
    # 省略
    end
    def cancel(fine_tune_id)
    # 省略
    end
    def list
      responses = client.finetunes.list
      responses["data"]
    end
    def retrieve(fine_tune_id)
      response = client.finetunes.retrieve(id: fine_tune_id)
      response
    end
    def completions(required_params)
      response = client.completions(
        parameters: {
        model: required_params[:fine_tuned_model],
        prompt: required_params[:prompt]
        }
      )
      response["choices"][0]["text"]
    end
    def delete(fine_tuned_model)
      response = client.finetunes.delete(fine_tuned_model: fine_tuned_model)
      response["id"]
    end
    private
    def add_parameters(file_id, options)
      parameters = {
        training_file: file_id,
        model: self.model
      }
    end
  end
end

解説
今回実装したコードの解説です。
まずは、listオプションを指定したコマンドの場合です。

RubyOpenAI::FineTuneクラスのlistメソッドを呼び出す

RubyOpenAI::FineTuneクラスのlistメソッド内で、OpenAI::Finetunesクラスのlistメソッドを呼び出す

レスポンスから「ファインチューンID・ステータス・ファインチューン済みのモデル」を順番にターミナルへ出力
つぎに、retrieveオプションを指定したコマンドの場合です。

ファインチューンIDを引数に、RubyOpenAI::FineTuneクラスのretrieveメソッドを呼び出す

RubyOpenAI::FineTuneクラスのretrieveメソッド内で、ファインチューンIDを引数にOpenAI::Finetunesクラスのretrieveメソッドを呼び出す

レスポンスから「ファインチューンID・ステータス・ファインチューン済みのモデル」をターミナルへ出力
そして、completionsオプションを指定したコマンドの場合です。

ユーザーが入力した「ファインチューン済みモデル名」と「プロンプト」を引数に、RubyOpenAI::FineTuneクラスのcompletionsメソッドを呼び出す

RubyOpenAI::FineTuneクラスのcompletionsメソッド内で、ファインチューン済みモデル名とプロンプトを引数に、OpenAI::Clientクラスのcompletionsメソッドを呼び出す

レスポンスから、プロンプトに対するファインチューン済みモデルの返答をターミナルへ出力
最後に、deleteオプションを指定したコマンドの場合です。

ユーザーが入力した「ファインチューン済みモデル名」を引数に、RubyOpenAI::FineTuneクラスのdeleteメソッドを呼び出す

RubyOpenAI::FineTuneクラスのdeleteメソッド内で、ファインチューン済みモデル名を引数に、OpenAI::Finetunesクラスのdeleteメソッドを呼び出す

レスポンスから、削除されたファインチューン済みモデル名をターミナルへ出力
挙動確認
まず、ruby sample_cli.rb finetune --listというコマンドを入力しましょう。
listオプションを指定した場合は、以下のようにファインチューニングの処理に入ったモデルを一覧で取得できます。
現在処理中のモデルは、ハイライト部分のようにstatusが「pending」となります。

ファインチューニングの処理に入ったモデルを取得できる

次にruby sample_cli.rb finetune --retrieveというコマンドを入力しましょう。
retrieveオプションを指定した場合は、以下のようにファインチューニングの処理に入ったモデルを個別に取得できます。

ファインチューニングの処理に入ったモデルを個別に取得できる

続いて、ruby sample_cli.rb finetune --completionsというコマンドを入力しましょう。
completionsオプションを指定した場合は、以下のようにファインチューニング済みのモデルを使用できます。
ここで、先ほど正しい返答が得られなかった「宇宙兄弟 南波六太の誕生日はいつですか?」というプロンプトを投げます。
すると、以下のように正しい誕生日情報が返ってきました。

ファインチューニング済みのモデルを使用できる

最後に、ruby sample_cli.rb finetune --deleteというコマンドを入力しましょう。
deleteオプションを指定した場合は、以下のようにファインチューニング済みのモデルを削除できます。

ファインチューニング済みのモデルを削除できる

Image

ruby-openaiでは、画像を操作するためのメソッドが以下3つ用意されています。
◎  Generate
テキストプロンプトを用いて画像生成を行います。
生成される画像サイズは、「256x256」「512x512」「 1024x1024」ピクセルです。サイズが小さいほど、生成速度が上がります。
また、パラメータの指定次第では一度に1~10 個の画像を生成できます。
◎ Edit
既存画像・プロンプトに加えてマスクをアップロードすることで、既存画像の編集および拡張を行います。
プロンプトでは、編集する画像全体の説明を行います。
マスクの透明な領域は、画像を編集する必要がある場所を示します。
アップロードされる画像とマスクは、どちらもサイズが4MB未満の正方形のPNG画像である必要があります。
下記OpenAIドキュメントに画像付きで示されているため、一読されることをお勧めします。

https://platform.openai.com/docs/guides/images/edits

◎ Variations
既存画像のバリエーションをn個生成します。
editと同様に、入力画像サイズは4MB未満であり、正方形のPNG画像である必要があります。
どれも出力としては操作された画像のURLが返りますが、1時間後に期限切れになります。
今回は、以下3つの機能を実装します。

  1. プロンプトで指示した内容をもとに、画像が生成される機能(Generate)
  2. プロンプト・元画像・マスク画像をもとに、編集された画像が生成される機能(Edit)

元画像・生成個数を伝えると、元画像のバリエーションがn個分生成される機能(Variations)

Generate

まずは、「プロンプトで指示した内容をもとに、画像が生成される機能」を実装します。
実装コード
■ sample_cli.rb

  desc "image", "Image API"
  def image
    client = OpenAI::Client.new
    image = RubyOpenAI::Image.new(client, model_version("babbage-similarity"))
    puts "Please input image you wish to generate."
    response = image.generate(prompt: gets_chomp)
    puts response
  end

■ ruby_openai/image.rb

module RubyOpenAI
  class Image
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def generate(required_params, options = {})
      response = client.images.generate(
        parameters: add_parameters(required_params, options)
      )
      response.dig("data", 0, "url")
    end
    private
    def add_parameters(required_params, options)
      parameters = {
        prompt: required_params[:prompt] 
      }
    end
  end
end

解説
今回実装したコードの解説です。

OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::Imageクラスのインスタンスを生成

ユーザーが入力した「プロンプト」を引数に、RubyOpenAI::Imageクラスのgenerateメソッドを呼び出し

RubyOpenAI::Imageクラスのgenerateメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
 ■ 例

 response = client.images.generate(
   parameters: { 
     prompt: "A baby sea otter cooking pasta wearing a hat of some sort", 
     size: "256x256"
   }
 )
 puts response.dig("data", 0, "url")
 # => "https://oaidalleapiprodscus.blob.core.windows.net/private/org-Rf437IxKhh..."


レスポンスから生成された画像URLを取得し、ターミナルへ出力
挙動確認
ruby sample_cli.rb imageというコマンドを入力しましょう。
プロンプトで生成したい画像のイメージを伝えると、以下のようにURLが返ります。

画像URLが生成された


今回は「森の中に潜む虎」というプロンプトを与えてみました。
生成された画像はこちら。

森の中に潜む(?)虎


森らしき場所ではありますが、心地良さそうに眠っている虎の画像が生成されました。
「緑いっぱいの森の中にいる、ちょっと怖そうな虎」の画像が生成されるかと想像していたため、期待する画像を得るためにはプロンプトを工夫しなければならなさそうです。
ちなみに、「緑いっぱいの森の中にいる、ちょっと怖そうな虎」と入力した場合、以下の画像が返りました。

緑いっぱいの森の中にいる、ちょっと怖そうな(?)虎


場所のイメージは改善されたものの、やはり心地良さそうに眠っています。

Edit

次に、「プロンプト・元画像・マスク画像をもとに、編集された画像が生成される機能」を実装します。
今回は、先ほど生成した「緑いっぱいの森の中にいる、ちょっと怖そうな虎」の画像を編集します。
実装コード
■ sample_cli.rb

  desc "image", "Image API"
  options :generation => :boolean, :edit => :boolean
  def image
    client = OpenAI::Client.new
    image = RubyOpenAI::Image.new(client, model_version("babbage-similarity"))
    case options.keys.join("")
    when "generation"
      puts "Please input image you wish to generate."
      response = image.generate(prompt: gets_chomp)
      puts response
    when "edit"
      puts "Please input image you wish to edit."
      input_prompt = gets_chomp
      puts "Please input image file."
      input_image_file = gets_chomp
      puts "Please input mask file."
      input_mask_file = gets_chomp
      response = image.edit(prompt: input_prompt, image: input_image_file, mask: input_mask_file)
      puts response
    end
  end
■ ruby_openai/image.rb
module RubyOpenAI
  class Image
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def generate(required_params, options = {})
    # 省略
    end
    def edit(required_params, options = {})
      response = client.images.edit(
        parameters: add_parameters(required_params, options)
      )
      response.dig("data", 0, "url")
    end
    private
    def add_parameters(required_params, options)
      parameters = {}
      parameters[:prompt] = required_params[:prompt] if required_params[:prompt]
      parameters[:image] = required_params[:image] if required_params[:image]
      parameters[:mask] = required_params[:mask] if required_params[:mask]
      parameters
    end
  end
end

最後に、git cloneしたディレクトリのルートディレクトリ配下に「『緑いっぱいの森の中にいる、ちょっと怖そうな虎』の画像」と「虎以外をマスク加工した画像」を用意しました。

画像を用意


解説
今回実装したコードの解説です。

画像生成系はruby sample_cli.rb image —オプションのコマンドで実行したいため、thorの機能であるoptionsを使用
    - これにより、画像生成を行いたい場合はruby sample_cli.rb image —generate、画像編集を行いたい場合はruby sample_cli.rb image —editのコマンドで実行することができます。

OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::Imageクラスのインスタンスを生成

ユーザーが入力した「プロンプト(元画像をどのように編集したいか)」・「元画像」・「マスク画像(元画像の編集したい部分にマスクをかけた画像)」を引数に、RubyOpenAI::Imageクラスのeditメソッドを呼び出し

RubyOpenAI::Imageクラスのeditメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
 ■ 例

 response = client.images.edit(
   parameters: { 
     prompt: "A solid red Ruby on a blue background",
     image: "image.png", mask: "mask.png"
   }
 )
 puts response.dig("data", 0, "url")
 # => "https://oaidalleapiprodscus.blob.core.windows.net/private/org-Rf437IxKhh..."


レスポンスから生成された画像URLを取得し、ターミナルへ出力
挙動確認
ruby sample_cli.rb image --editというコマンドを入力しましょう。
プロンプトで生成したい画像のイメージを伝えると、以下のようにURLが返ります。

画像URLが生成された


今回は「青空の中に浮かぶ雲の上で、心地良さそうに寝る虎」というプロンプトで、元画像とマスク画像を与えてみました。
生成された画像はこちら。

青空の中に浮かぶ雲の上で(?)、心地良さそうに寝る虎


こちらも期待する結果(雲の上で寝ている画像)を得るためには、よりプロンプトを工夫しなければならなさそうです。

Variations

最後に、「元画像・生成個数を伝えると、元画像のバリエーションがn個分生成される機能」を実装します。
実装コード
■ sample_cli.rb

  desc "image", "Image API"
  options :generation =>:boolean, :edit => :boolean, :variations => :boolean
  def image
    client = OpenAI::Client.new
    image = RubyOpenAI::Image.new(client, model_version("babbage-similarity"))
    case options.keys.join("")
    when "generation"
    # 省略
    when "edit"
    # 省略
    when "variations"
      puts "Please input image file."
      input_image_file = gets_chomp
      puts "Please input the number of images you wish to generate."
      input_number = gets_chomp.to_i
      response = image.variations(image: input_image_file, n: input_number)
      puts response
    end
  end

■ ruby_openai/image.rb

module RubyOpenAI
  class Image
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def generate(required_params, options = {})
    # 省略
    end
    def edit(required_params, options = {})
    # 省略
    end
    def variations(required_params, options = {})
      response = client.images.variations(
        parameters: add_parameters(required_params, options)
      )
      responses = []
      required_params[:n].times do |i|
        responses << response.dig("data", i, "url")
      end
      responses
    end
    private
    def add_parameters(required_params, options)
      parameters = {}
      parameters[:prompt] = required_params[:prompt] if required_params[:prompt]
      parameters[:image] = required_params[:image] if required_params[:image]
      parameters[:mask] = required_params[:mask] if required_params[:mask]
      parameters[:n] = required_params[:n] if required_params[:n]
      parameters
    end
  end
end

解説
今回実装したコードの解説です。

ユーザーが入力した「元画像」・「生成個数」を引数に、RubyOpenAI::Imageクラスのvariationsメソッドを呼び出し

RubyOpenAI::Imageクラスのvariationsメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
    ■ 例

 response = client.images.variations(
   parameters: {
     image: "image.png", 
     n: 2 # 生成個数
   }
 )
 puts response.dig("data", 0, "url")
 # => "https://oaidalleapiprodscus.blob.core.windows.net/private/org-Rf437IxKhh..."


レスポンスから生成された画像URLを取得し、順番にターミナルへ出力
挙動確認
ruby sample_cli.rb finetune --variationsというコマンドを入力しましょう。
元画像と生成個数を入力すると、以下のようにURLが返ります。
今回は、先ほど生成した「緑いっぱいの森の中にいる、ちょっと怖そうな虎」の画像を元画像とし、個数には「3」を入力しました。

画像URLが3つ生成された


生成された3つの画像はこちら。

3種類の画像


確かに全て異なる画像ですが、もう少しわかりやすい異なり方をするのかと想像していました。
元画像次第では、わかりやすい異なり方をするかもしれませんね。

Moderation

Moderationは、テキストがOpenAIの使用ポリシーに違反しているかどうかを分類します。
Moderationを使用することで、開発者は使用ポリシーで禁止されているテキストを特定できるため、フィルタリングなどの措置に役立てることができます。
具体的には、以下の内容がOpenAIの使用ポリシーとして禁止されています。
| カテゴリ | 内容 |
| --- | --- |
| hate | 人種、性別、民族、宗教、国籍、性的指向、障害の有無、またはカーストに基づく憎しみを表現、扇動、または助長するテキスト。 |
| hate/threatening | 対象グループに対する暴力や重大な危害を含む憎悪に満ちたテキスト。 |
| self-harm | 自殺、切断、摂食障害などの自傷行為を促進、奨励、または描写するテキスト。 |
| sexual | 性行為の説明など、性的興奮を喚起することを目的としたテキスト、または性的サービスを宣伝するテキスト(性教育や健康を除く)。 |
| sexual/minors | 18 歳未満の個人が含まれる性的なテキスト。 |
| violence | 暴力を促進または美化するコンテンツ、または他者の苦しみや屈辱を称賛するテキスト。 |
| violence/graphic | 死、暴力、重篤な身体的傷害を極めて生々しい詳細で描写する暴力的なテキスト。 |
Moderationを使用すると、以下3つのことがわかります。

テキストがポリシー違反しているか(全体)

テキストがポリシー違反しているか(カテゴリ毎)

テキストに対するカテゴリ毎のポリシー違反具合
また、OpenAI API の入力と出力を監視する際には無料で使用できますが、現在時点ではサードパーティのトラフィックの監視はサポートされていない点に注意が必要です。
今回はmoderationを用いて、「入力されたプロンプトの内容が、全カテゴリを対象にポリシー違反をしているか確認する」という機能を実装します。
実装コード
■ sample_cli.rb

  desc "moderation", "Moderation API"
  def moderation
    puts "Please input your message."
    client = OpenAI::Client.new
    moderation = RubyOpenAI::Moderation.new(client, model_version)
    response = moderation.get_response(input: gets_chomp)
    if response
      puts "This message violates policy."
    else
      puts "This message does not violate policy."
    end
  end
■ ruby_openai/moderation.rb
module RubyOpenAI
  class Moderation
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def get_response(required_params, options = {})
      response = client.moderations(
        parameters: add_parameters(required_params, options)
      )
      response.dig("results", 0, "flagged")
    end
    private
    def add_parameters(required_params, options)
      parameters = {
        input: required_params[:input],
      }
    end
  end
end

解説
今回実装したコードの解説です。

OpenAI::Clientクラスのインスタンスとモデルを引数に、RubyOpenAI::Moderationクラスのインスタンスを生成

ユーザーが入力した「プロンプト」を引数に、RubyOpenAI::Moderationクラスのget_responseメソッドを呼び出す

RubyOpenAI::Moderationクラスのget_responseメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
    ■ 例

 response = client.moderations(
   parameters: { 
     input: "I'm worried about that." 
   }
 )
 puts response.dig("results", 0, "category_scores", "hate")
 # => 5.505014632944949e-05

レスポンスから必要な値(flaggedの値)を取得し、値が「true」であればポリシー違反、「false」であればポリシー違反ではない旨のメッセージをターミナルへ出力
    - レスポンスは以下の値となっています。全カテゴリに対するポリシー違反を検知する際は、「flagged」の値をメソッドの返り値とします。
    ■ 例
挙動確認

 {
   "id": "modr-5MWoLO",
   "model": "text-moderation-001",
   "results": [
     {
       "categories": {
         "hate": false,
         "hate/threatening": true,
         "self-harm": false,
         "sexual": false,
         "sexual/minors": false,
         "violence": true,
         "violence/graphic": false
       },
       "category_scores": {
         "hate": 0.22714105248451233,
         "hate/threatening": 0.4132447838783264,
         "self-harm": 0.005232391878962517,
         "sexual": 0.01407341007143259,
         "sexual/minors": 0.0038522258400917053,
         "violence": 0.9223177433013916,
         "violence/graphic": 0.036865197122097015
       },
       "flagged": true
     }
   ]
 }

ruby sample_cli.rb moderationというコマンドを入力しましょう。
今回は「盗んだバイクで走り出します」というテキストに対して、ポリシー違反の検証を行いました。
結果は、下記画像の通り「This message does not violate policy.」というメッセージが返り、ポリシー違反はしていないことが確認できました。

テキストがポリシー違反するかどうか検証


現状、相当直接的な表現でない限り、ポリシー違反にはならないように感じられました。
※ OpenAIのドキュメント記載の例を用いると、ポリシー違反と判定されます。

https://platform.openai.com/docs/api-reference/moderations/create

Whisper

Whisperとは、オーディオファイルをテキストへ変換することができる音声認識モデルです。
Whisperを使用することで、以下2つの機能を使用することができます。
◎ Translate(翻訳)
サポートされている自然言語(日本語・ドイツ語など)の音声ファイルを入力として受け取ると、その音声を必要に応じて英語に翻訳した上で書き起こしを行います。
例えば「ドイツ語の音声ファイル」を渡すと「英語に翻訳されたテキスト」が返ってきます。
現時点では英語への翻訳のみをサポートしているため、英語以外の言語への翻訳はできません。
◎ Transcribe(転写)
文字起こししたい音声ファイルと、音声の文字起こしに必要な出力ファイル形式を入力として受け取ると、その音声を転写したテキストを返します。
こちらは、翻訳が挟まれないため、日本語音声であれば日本語のテキストが返ります。
今回は、translateとtranscribeの機能を用いて、以下2つの機能を実装します。

  1. 日本語収録した音声ファイルを渡すと、英訳されたテキストが返ってくる機能

日本語収録した音声ファイルを渡すと、文字起こしされたテキストが返ってくる機能

Translate

まずは、「日本語収録した音声ファイルを渡すと、英訳されたテキストが返ってくる機能」を実装します。
実装コード
■ sample_cli.rb

  desc "translate", "Translate API"
  def translate
    puts "Please input audio file path."
    client = OpenAI::Client.new
    translate = RubyOpenAI::Translate.new(client, model_version("whisper-1"))
    response = translate.get_response(file: gets_chomp, extension: "rb")
    puts response
  end

■ ruby_openai/translate.rb

module RubyOpenAI
  class Translate
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def get_response(required_params, options = {})
      response = client.translate(
        parameters: add_parameters(required_params, options)
      )
      response["text"]
    end
    private
    def add_parameters(required_params, options)
      parameters = {
        model: self.model,
        file: ::File.open(required_params[:file], required_params[:extension])
      }
    end
  end
end

最後に、git cloneしたディレクトリのルートディレクトリ配下に、音声ファイルを用意しましょう。
今回の例では「私は猫を飼っています」という音声が入ったファイルを用意しました。録音はMacに最初から入っている「Quick Time Player」というアプリで行いました。

音声ファイルを用意


解説
今回実装したコードの解説です。

OpenAI::ClientクラスのインスタンスとWhisperモデルを引数に、RubyOpenAI::Translateクラスのインスタンスを生成

ユーザーが入力した「ファイルのパス」と「ファイル拡張子」を引数に、RubyOpenAI::Translateクラスのget_responseメソッドを呼び出す

RubyOpenAI::Translateクラスのget_responseメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
    ■ 例

response = client.translate(
  parameters: {
    model: "whisper-1",
    file: File.open("path_to_file", "rb"),
  }
)
puts response["text"]

=> "Translation of the text"

レスポンスから翻訳されたテキストを取得、ターミナルへ出力
挙動確認
ruby sample_cli.rb translateというコマンドを入力しましょう。
音声ファイルのパスを入力すると、英訳されたテキストが返ってきます。

「私は猫を飼っています」を英訳したテキストが返ってくる

Transcribe

次は、「日本語収録した音声ファイルを渡すと、文字起こしされたテキストが返ってくる機能」を実装します。
実装コード
■ sample_cli.rb

 desc "transcribe", "Transcribe API"
  def transcribe
    puts "Please input audio file path."
    client = OpenAI::.new
    transcribe = RubyOpenAI::Transcribe.new(client, model_version("whisper-1"))
    response = transcribe.get_response(file: gets_chomp, extension: "rb")
    puts response
 end

■ ruby_openai/transcribe.rb

module RubyOpenAI
  class Transcribe
    attr_reader :client, :model
    def initialize(client, model)
      @client = client
      @model = model
    end
  
    def get_response(required_params, options = {})
      response = client.transcribe(
        parameters: add_parameters(required_params, options)
      )
      response["text"]
    end
    private
    def add_parameters(required_params, options)
      parameters = {
        model: self.model,
        file: ::File.open(required_params[:file], required_params[:extension])
      }
    end
  end
end

解説
今回実装したコードの解説です。

  1. OpenAI::ClientクラスのインスタンスとWhisperモデルを引数に、RubyOpenAI::Transcribeクラスのインスタンスを生成
  2. ユーザーが入力した「ファイルのパス」と「ファイル拡張子」を引数に、RubyOpenAI::Transcribeクラスのget_responseメソッドを呼び出す
  3. RubyOpenAI::Transcribeクラスのget_responseメソッド内で、以下のようなパラメータを用いてOpenAIへリクエストを行う
    ■ 例

    response = client.transcribe(
      parameters: {
        model: "whisper-1",
        file: File.open("path_to_file", "rb"),
      }
    )
    puts response["text"]

    => "Transcription of the text

  4. レスポンスから転写されたテキストを取得し、ターミナルへ出力

挙動確認
ruby sample_cli.rb transcribeというコマンドを入力しましょう。
音声ファイルのパスを入力すると、文字起こしされたテキストが返ってきます。

「私は猫を飼っています」という音声の文字起こしが行われた


「Translate」の際に使用した音声ファイルを用いたため、「私は猫を飼っています」というテキストが返ってきました。
内容だけでなく、漢字の間違いもなく返ってきました。


感想・まとめ

本記事の執筆を通して最も感じたことは、実際に触れ、アウトプットすることの大切さです。
今回は、ruby-openaiのソースコードとOpen AIの公式ドキュメントを比較しながら、サンプルアプリを作成しました。
公式ドキュメントを読む習慣は勿論大切ですが、実際に手を動かしながらアウトプットを行うことで、Open AIやruby-openaiに対する理解がとても深まったと感じています。

おわりに

DIVXでは一緒に働ける仲間を募集しています。
興味があるかたはぜひ採用ページを御覧ください。

  採用情報 | 株式会社divx(ディブエックス) 可能性を広げる転職を。DIVXの採用情報ページです。 株式会社divx(ディブエックス)



お気軽にご相談ください


ご不明な点はお気軽に
お問い合わせください

サービス資料や
お役立ち資料はこちら

DIVXブログ

テックブログ タグ一覧

人気記事ランキング

GoTopイメージ