2018-03-22

AIで似ているAV女優を紹介しているスケベAI「スケベ博士」を作りました。②実装編

AIで似ているAV女優を紹介しているスケベAI「スケベ博士」を作りました。①アプリ紹介編 の続きです。実際にどう実装したかという話です。

まだ友達追加していない人はここからチェケラ

https://line.me/R/ti/p/6XAcvOifDl





AIで似ているAV女優を紹介しているスケベAI「スケベ博士」を作りました。①アプリ紹介編

こんにちは。スケベサイエンティストのDAI(@never_be_a_pm)です。 AIで、画像から似ているAV女優を紹介してくれるLine Bot「スケベ博士」を作りました。 背景 私たち男性がスケベするときに、よく妄想しますよね。 中学生の頃は「○○ちゃん」のことを想像しながらスケベしておりました。 問題点 ...

実装技術編


前処理編



まず、前処理です。流れとしては、


  • PythonでエロサイトからAV女優名とサムネイル画像をスクレイピング
  • 取得したデータをCSVに保存
  • PythonでCSVからAV女優名のクエリをDMM APIに投げて、その女優の画像をもう一枚取得
  • それらのデータをMicroSoft Face APIで学習させる


といったことをしました。


前処理については別の記事でまとめているので、参照してみてください。
また、その過程ででてきた変態コンシェルジュや、ビッグデータ解析等も興味があったら読んでみて下さい。


Python 機械学習のための画像収集①:アダルトサイトからAV女優6000名の名簿データをスクレイピングし、スプレッドシートを公開しました



Python 機械学習のための画像収集①:アダルトサイトからAV女優6000名の名簿データをスクレイピングし、スプレッドシートを公開しました

やりたいこと Microsoft AzureのFace APIを使って、Lineから送られてきた画像から、似ているAV女優を取得する 


Python 機械学習のための画像収集②:Pythonで、DMMの女優検索APIから、AV女優2840名の身長・バスト・カップ数・ウェスト・ヒップ・生年月日データを取得しました。


Python 機械学習のための画像収集②:Pythonで、DMMの女優検索APIから、AV女優2840名の身長・バスト・カップ数・ウェスト・ヒップ・生年月日データを取得しました。

やりたいこと Microsoft AzureのFace APIを使って、Lineから送られてきた画像から、似ているAV女優を取得したい
Python 機械学習のためのデータ処理:AV女優2609件のデータから、欠損値を処理する


Python 機械学習のためのデータ処理:AV女優2609件のデータから、欠損値を処理する

Pythonを利用して、取得したAV女優2609件のデータに欠損値が含まれていたので、pandasで欠損値の前処理を行った。 #前回までの流れ AV女優の詳細情報を取得した。

AV女優2237名のビッグデータから見えてきた、アダルトビデオに関する驚愕の5つ真実

AV女優2237名のビッグデータから見えてきた、アダルトビデオに関する驚愕の5つ真実

前回までに、 アダルト動画ナビ からAV女優6000名のデータをスクレイピングしました。さらに、その結果をもとにDMMのAPIから、AV女優の身長・バスト・カップ数・ウェスト・ヒップ・生年月日データ等の詳細情報を取得しました。 やりたいこと Microsoft AzureのFace APIを使って、Lineから送られてきた画像から、似ているAV女優を取得する やりたいこと Microsoft


Python 機械学習のためのMicrosoft Azure Face API活用① Microsoft Azure Face APIに、AV女優を学習させるためにAPIドキュメントを読む

Python 機械学習のためのMicrosoft Azure Face API活用① Microsoft Azure Face APIに、AV女優を学習させるためにAPIドキュメントを読む

Microsoft Azure Face APIを利用して、スクレイピングで取得したAV女優の画像を学習させる。 #TODO この記事を参考にして、Microsoft Face APIにAV女優の画像を学習させる 今回はMicrosoft Azure Face APIを利用してみたいので、そのドキュメントを読んでまとめてみる Face APIで登録した顔の人物情報を取得する Pythonのrequestsを利用して、Web APIを利用する 具体的な処理の流れ Person Groupを作成する Personを作成する 顔を登録する 最後にPerson Groupを学習させる Person Groupを作成する PersonGroup - Create Create a new person group with specified personGroupId, name, and user-provided userData. A person group is the container of the uploaded person data, including face images and face recognition features.


Python 機械学習のためのMicrosoft Azure Face API活用② Microsoft Azure Face APIに、AV女優を学習させる

Python 機械学習のためのMicrosoft Azure Face API活用② Microsoft Azure Face APIに、AV女優を学習させる

とりあえずメモで。 PythonでMicrosoft Azure Face APIを利用して、画像の学習が成功したので、とりあえず動かしてみたときのコードを置いておく。 Face APIで鳳かなめの画像を学習させた後に、山田花子と鳳かなめの画像を送信。山田花子の場合、一致度は0%だったが、鳳かなめの場合、別の画像だと55%になった。実装したときに気になるのは、まったく関係ない人を入れたときに類似度がちゃんと見れるのかってことだなぁ。




実行処理編



で、実際に実行するときの処理です。

  1. Line Messaging APIから、女の子の画像のURLを送信
  2. Google Apps Scriptで画像URLを受信、Microsoft Azure Face APIでURLを投げる
  3. Face API上で画像から似ているAV女優名を取得しGoogle Apps Scriptで返す
  4. Google Apps ScriptからAV女優名をもとにDMM APIで女優の画像と女優の商品一覧リストを検索


という流れになっています。



この編の流れは、前にLineからDMMのサンプル動画が見れる、変態コンシェルジュと構造は似ているので、こちらを参考にしてみてください。


検索したDMMアダルトのサンプル動画をLINEからすぐ見れる「変態コンシェルジュ」を作ってみました

検索したDMMアダルトのサンプル動画をLINEからすぐ見れる「変態コンシェルジュ」を作ってみました

問題提起 こんにちは、DAIです。(@never_be_a_pm) アダルト動画の品質は、ここ数年間でかなり品質がよくなってきています。それにつれて、サンプル動画の質も非常によくなってきています。 ...


全体を簡単にまとめると


  • LINE Messaging APIがView
  • Google Apps ScriptがController
  • Face API、DMM APIが Model

の役割を果たしていると思います。

マッシュアップアプリは面白い

若干実装よりの話から離れますが
いやぁマッシュアップアプリつくるの面白いな。簡単にできて。



実装の話としてLine APIを利用したのは、いろいろとメリットがあるんですよね。






Web APIでマッシュアップして作ったほうが、作るのも楽だし、利用する側も既存のプラとフォームで使えるので学習コストが低いですね。いちからWebサイト作ったら、UI考えたりとかしなきゃいけなくて面倒です。でもLineのインターフェイスなら人がもっとアクセスしやすいだろうし、実装も楽だし、いいことづくしなんですよね。そのうちLine Payとかも入ってきて、Webのインターフェイスよりもこっちのほうが身近になるような気がします。




ただ精度を上げたり、スケールするためには既存の大きなプラットフォームに金払わなければならないので、AWS 植民地支配みたいな感じになるところがつらいところです。ということで、リビドードリブン開発はお金にならないので、こちらのページで本当に頭おかしいか性欲がやばい人は募金してください!募金の結果は、僕が私利私欲で使うかもしれませんが、かなり集まったらFace APIの有料プランに登録して、もっと多くの画像を学習させたり、人やとってひたすらAV女優の画像データ集めさせたりしようと思っています。


スケベAI「変態博士」の献金お願いします!|Dai|note

Microsoft Azure Face APIをより精度の高い学習データを利用できるように献金してください。献金されたお金は、たぶんAPIの利用料に払います。もしかしたら僕が私利私欲のために使うかもしれないです。


(ちなみに@budehucさんが募金してくださいました。本当スケベは金持ってないので助かります。Special Thanks!)





話はそれましたが、こういうWeb APIを組み合わせたマッシュアップアプリは結構初心者でも作れるので(ちなみに私もプログラマーではなく、趣味でちょこっと書いているくらいなので)試してみたら面白いと思います。前に書いたポエム記事ですがマッシュアップとはこんな感じです。


Web APIのマッシュアップアプリ開発入門


Google Apps Scriptのロジック


Line APIからWeb hockで受け取ったリクエストは、doPost関数で取得するのですがその辺わかりにくいので、この記事を読んでくださいと。


Google Apps ScriptのdoGet関数・doPost関数を解説

Google Apps ScriptのdoGet関数・doPost関数を解説

Google Apps ScriptのdoGet() doPost()について解説 Google Apps ScriptのページのURLに、GET・POSTリクエストが送られたときに、関数を実行することができます

で、実際のコードなのですが、まだ未完成でいらない部分やリファクタリングしてない部分のコードが多々あるので、それはなんとか頑張って読んで理解してください。


// プロパティ取得
var PROPERTIES = PropertiesService.getScriptProperties();//ファイル > プロジェクトのプロパティから設定した環境変数的なもの

//Google Documentにログファイルを保存

var GOOGLE_DOCUMENT_ID = PROPERTIES.getProperty("GOOGLE_DOCUMENT_ID")

//Google Driveの画像を保存するフォルダの設定

var GOOGLE_DRIVE_FOLDER_ID = PROPERTIES.getProperty('GOOGLE_DRIVE_FOLDER_ID')

//LINE・DMMの設定をプロジェクトのプロパティから取得
var LINE_ACCESS_TOKEN = PROPERTIES.getProperty('LINE_ACCESS_TOKEN')
var LINE_END_POINT = "https://api.line.me/v2/bot/message/reply"

//GYASOの設定

var GYASO_ACCESS_TOKEN = PROPERTIES.getProperty("GYASO_ACCESS_TOKEN")

//MicroSoft Azure Face APIの設定
var FACE_API_SUBSCRIPTION_KEY = PROPERTIES.getProperty('FACE_API_SUBSCRIPTION_KEY')
var FACE_API_PERSON_GROUP = "avactress"
var FACE_API_BASE_END_POINT = "https://westcentralus.api.cognitive.microsoft.com/face/v1.0/"

//DMMの設定
var DMM_API_ID = PROPERTIES.getProperty('DMM_API_ID')
var DMM_AFFILIATE_ID = PROPERTIES.getProperty('DMM_AFFILIATE_ID')

var reply_token;
var imageUrl;
var id;

//ログファイルの設定

var logFile = DocumentApp.openById(GOOGLE_DOCUMENT_ID);

//LINEのエンドポイント

function doGet() {
  return HtmlService.createTemplateFromFile("test").evaluate();
}

/* 処理内容

  ・LINEから画像バイナリファイルを取得
  ・バイナリファイルをMicrosoft Azure Face APIに送信
  ・Face APIから取得したAV女優名をもとに、DMM APIから女優の画像とリンクを取得
  ・LINEに 
    ・AV女優名
    ・合致度
    ・女優画像
    ・女優URLを返信
  ・合致しなかった場合、女優追加申請フォームを返す。
*/


//LINEからPOSTリクエストを受けたときに起動する
function doPost(e){

  if (typeof e === "undefined"){
    /*
     * debug用の処理です
     * imageUrlに、任意のAV女優の画像を挿入しています。
    */
    imageEndPoint = "http://eropalace21.com/wordpress/wp-content/uploads/2016/01/sakuramana_thumb.jpg" //検証用の画像
  } else {

    /*
     * Lineからメッセージが送られたときの処理です
     * LineのmessageIdを取得し、そこからバイナリ形式の画像データを取得します
    */

    //messageIdから、Line上に存在するバイナリ形式の画像URLを取得します

    var json = JSON.parse(e.postData.contents);
    reply_token= json.events[0].replyToken;
    //var messageId = json.events[0].message.id;
    imageEndPoint = json.events[0].message.text;    

    //imageEndPoint = 'https://api.line.me/v2/bot/message/'+ messageId +'/content/' //バイナリファイルの画像が取得できるエンドポイント
  }
    Logger.log("以下のURLから、画像を取得します: " + imageEndPoint)
    console.log(imageEndPoint)

    //画像のエンドポイントから、バイナリ形式でデータを取得
    //imageBlob = getImageBlobByImageUrl(imageEndPoint);
    //imageUrl = saveImageBlobAsPng(imageBlob)
    //imageUrl = getImageUrl(imageBlob)
    //saveImageBlobAsPng(imageBlob)

    //画像データから、女優名(name)、合致度(confidence)、プロフィール画像(profileImageUrl), 女優の商品画像リスト(itemsInfoUrl)を取得します
    var faceId = detectFaceId(imageEndPoint)
    var personIdAndConfidence = getPersonIdAndConfidence(faceId)
    var personId = personIdAndConfidence["personId"]
    var confidence = personIdAndConfidence["confidence"]
    var name = getActressName(personId)
    var profileImageUrlAndItemsInfoUrl = getProfileImageUrlAndItemsInfoUrl(name)
    var profileImageUrl = profileImageUrlAndItemsInfoUrl["profileImageUrl"]
    var itemsInfoUrl = profileImageUrlAndItemsInfoUrl["itemsInfoUrl"]
    //LineにAV女優名・一致度・女優の画像・女優のAVリストを送信します
    sendLine(name, confidence, profileImageUrl, itemsInfoUrl)
    logFile.getBody().appendParagraph(Logger.getLog());

}

function detectFaceId(uri){

  end_point = FACE_API_BASE_END_POINT + "detect"

  try {
    payload = {
      "url":uri
    }
    headers = {
      "Ocp-Apim-Subscription-Key": FACE_API_SUBSCRIPTION_KEY,
      "Content-Type": "application/json"
    };

    var res = UrlFetchApp.fetch(
      end_point,
      {
        'method': 'POST',
        'headers': headers,
        'payload': JSON.stringify(payload)
      }
    );

    res = JSON.parse(res)
    faceId = res[0]["faceId"]
    Logger.log("faceId: " + faceId)
    return faceId

  } catch (e){
    Logger.log("faceIdの取得に失敗しました")
    Logger.log("エラーメッセージ:" + e)
    return e
  }
}


function getPersonIdAndConfidence(faceId){

  /*
  * faceIdから、personIdとconfidenceを取得します
  * @params
    - faceId{String}: 画像から検出されたfaceId
  * @return
    - personIdAndoConfidence{array}
      - personId
      - concidence
  */

  end_point = FACE_API_BASE_END_POINT + "identify"

  try{
      faceIds = [faceId] //faceIdsはリストで送信される
      payload = {
        "faceIds" :faceIds,
        "personGroupId" :FACE_API_PERSON_GROUP,
      }

      res = UrlFetchApp.fetch(
        end_point,
        {
          'method': 'POST',
          'headers': headers,
          'payload': JSON.stringify(payload)
          //'payload': payload
        }
      );

      res = JSON.parse(res)

      var personId = res[0]["candidates"][0]["personId"]
      var confidence = res[0]["candidates"][0]["confidence"]
      Logger.log("personIdを取得しました: " + personId )
      Logger.log("coincidenceを取得しました: " + confidence)

      personIdAndConfidence = {
        "personId": personId,
        "confidence": confidence
      }
      return personIdAndConfidence;
    } catch (e){
      Logger.log("personId・confidenceの取得に失敗しました")
      Logger.log(e)
      return e
  }
}

function getActressName(personId){


  /*
   * Face APIから取得したpersonIdから、女優名を取得します
   * @ param
   *  - personId: Face APIで学習したpersonに紐づけられたID
   * @ return
   *  - name{string}: 女優名をフルネームで返します
  */

  end_point = FACE_API_BASE_END_POINT + "persongroups/" + FACE_API_PERSON_GROUP + "/persons/" + personId

  try {
    res = UrlFetchApp.fetch(
      end_point,
      {
        'method': 'GET',
        'headers': headers
      }
    );
    res = JSON.parse(res)
    name = res["name"] //女優名
    Logger.log("女優名を取得しました: " + name)
    return name;
  } catch (e){
    Logger.log("女優名を取得できませんでした")
    Logger.log(e)
    return e
  }
}


function getProfileImageUrlAndItemsInfoUrl(name){

  /*
  * AV女優名(name)から、DMMのAPIをかませて、女優の詳細データを取得します
  @param
    - name{String}: 女優名
  @return
    - actressInfo{array}:AV女優の以下の情報を取得
      - profileImageUrl{String}: 女優のプロフィール画像
      - itemsInfoUrl{String}: 女優が出演しているAVリストのURL
  */

  /* DMM APIから、女優名をもとに、サンプル動画のURLを取得
  */

  try {

    var encoded_query = encodeURI(name); //パーセントエンコーディングを行う
    var DMM_end_point = "https://api.dmm.com/affiliate/v3/ActressSearch?"
       + "api_id=" + DMM_API_ID
       + "&affiliate_id=" + DMM_AFFILIATE_ID
       + "&keyword=" + encoded_query
       + "&output=json"
    var response = UrlFetchApp.fetch(DMM_end_point)
    var txt = response.getContentText();
    var json = JSON.parse(txt);
    var actress = json.result.actress[0]
    var profileImageUrl = actress.imageURL.large
    profileImageUrl = profileImageUrl.replace(/^http?\:\/\//i, "https://");
    Logger.log("プロフィール画像を取得しました: " + profileImageUrl)
    var itemsInfoUrl = actress.listURL.digital
    itemsInfoUrl = itemsInfoUrl.replace(/^http?\:\/\//i, "https://");
    Logger.log("女優情報詳細ページURLを取得しました: " + itemsInfoUrl)
    var profileImageUrlAndItemsInfoUrl = {
      "profileImageUrl":profileImageUrl,
      "itemsInfoUrl": itemsInfoUrl
    }
    return profileImageUrlAndItemsInfoUrl;
  } catch (e){
    Logger.log("プロフィール写真と、女優情報詳細ページURLが取得できませんでした")
    return e
  }
}

function getImageBlobByImageUrl(url){

  /* LineのメッセージIDから、送られた画像をBlob形式で取得します、
   * @params
    - url{string}: 取得したい画像のURLです
   * @return
   * - imageBlob<string>: Blob形式で取得した画像ファイル
  */

  try {
    var res = UrlFetchApp.fetch(url, {
      'headers': {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN,
      },
      'method': 'get'
    });

    var imageBlob = res.getBlob().getAs("image/png").setName("temp.png")
    Logger.log("imageBlobの取得に成功しました")
    Logger.log("ContentType:" + imageBlob.getContentType())
    Logger.log("Name: " + imageBlob.getName())
    //Logger.log("")
    return imageBlob;

    /*
    var binaryData = res.getContent()
    var imageBlob = Utilities.newBlob(binaryData, 'image/png', 'MyImageName');
    return imageBlob
    */
  } catch(e) {
    Logger.log("バイナリ形式の画像取得に失敗しました")
    Logger.log("エラーメッセージ:" + e)
    return e
  }
}

function sendLine(name, coincidence, actressImageUrl, actressInfoUrl){

  Logger.log("name: "+ name)
  Logger.log("coincidence: "+ coincidence)
  Logger.log("actressImageUrl: "+ actressImageUrl)
  Logger.log("actressInfoUrl:" + actressInfoUrl)

  if (typeof coincidence === "undefined"){
    var messages = [{
      "type": "template",
      "altText": "すまん、みつからんかったのじゃ",
      "template": {
        "type": "buttons",
        "thumbnailImageUrl": 'https://rr.img.naver.jp/mig?src=http%3A%2F%2Fimgcc.naver.jp%2Fkaze%2Fmission%2FUSER%2F20160319%2F73%2F7666243%2F444%2F400x400xbb8969833802de4d23d8397c.jpg',
        "title": "あなたのスケベな願望に答えられませんでした。",
        "text": "一致するAV女優が見つかりませんでした。憤りを抑えられない方は献金してください",
        "actions": [
        {
        "type": "uri",
        "label": "献金する",
        "uri": "https://note.mu/daikawai/n/nc393f0355579"
      }
      ]
    }
    }];

  } else{
    var messages = [{
      "type": "template",
      "altText": "おすすめのAV女優はこれじゃ。",
      "template": {
        "type": "buttons",
        "thumbnailImageUrl": actressImageUrl,
        "title": name,
        "text": "一致度は" + (Math.round(coincidence * 100)) + "%じゃ",
        "actions": [
        {
        "type": "uri",
        "label": "動画一覧ページに移動!",
        "uri": actressInfoUrl
      }
      ]
    }
    }];
   }
  try {
    UrlFetchApp.fetch(LINE_END_POINT, {
      'headers': {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN,
      },
      'method': 'post',
      'payload': JSON.stringify({
        'replyToken': reply_token,
        'messages': messages,
      }),
    });
    return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);  
  } catch (e){
    Logger.log("LINEへのメッセージ送信に失敗しました")
    Logger.log(e)
  }
}

function saveImageBlobAsPng(imageBlob){

  /*
    @params
      - imageBlob
    @void
      - Google Drive上の指定されたフォルダに画像を保存します
  */

    try{
    var folder = DriveApp.getFolderById(GOOGLE_DRIVE_FOLDER_ID);
    var file = folder.createFile(imageBlob);
    Logger.log("[INFO] Google Driveに以下のURLに画像が保存されました: " + folder.getUrl())
    Logger.log("file.getUrl():" + file.getUrl())
    return file.getUrl()
  } catch (e){
    Logger.log("[ERROE]Google Driveに画像を保存できませんでした")
    Logger.log(e)
  }

}


function getImageUrl(imageBlob){
  /* 
   * GyasoにImageBlobを投げて、その画像のURLを取得する
   * @params
   * - imageBlob {Blob}:
   * @return 
   * - imageUrl {string}:  
  */

  GYASO_END_POINT = "https://upload.gyazo.com/api/upload?access_token=" + GYASO_ACCESS_TOKEN
  Logger.log(imageBlob.getDataAsString("UTF16"))
  try{
    var res = UrlFetchApp.fetch(GYASO_END_POINT, {
      'method': 'post',
      'headers': {
        'Content-Type': 'multipart/form-data;'
      },
      'payload': {
        'imagedata': imageBlob.getDataAsString()
      }
    });
    res = JSON.parse(res)
    Logger.log(res)
  } catch (e){
    Logger.log(e)
  }


}


あいかわらずプログラマーから見たらマサカリが飛んできそうなコードです。僕のコードレビューしてくれる人募集です。


次のリリースに向けて

まだやり残していることがたくさんあるので、リビドードリブンで実装する。

  • Lineから画像ファイル指定で直で検索できるようにしたい ⇒ Google Apps Scriptでバイナリ形式で画像をPOSTする方法がわからなかったため、いったんストップ中
  • エラーリスポンスをより正確にしたい。画像ファイルの拡張子がだめなのか、画像が顔認識できないのか、そもそも似ているひとがいないのかがいまのレスポンスだとわからないので、そこの処理を明確にしたい。
  • 学習する画像データの数と質を加えたい。現在スクレイピングで撮ってきた4000枚だが、一つ一つチェックしていないし、ものによっては画像があらすぎてはじかれているものが多い
  • 現役でないおばさんのAV女優を除外する。昭和の香りがするAV女優はミレニアム世代からすると萎える(ここは炎上する可能性がある)現役のAV女優だけ残すには、人の手が必要なので、人件費が集まったらスケベ画像を集めるバイト雇いたい


よりよいスケベなサービスにするために、APIの利用費と、エロ画像を集める外注費用を集めるために、募金しています。よろしくお願いします。



スケベAI「変態博士」の献金お願いします!|Dai|note

Microsoft Azure Face APIをより精度の高い学習データを利用できるように献金してください。献金されたお金は、たぶんAPIの利用料に払います。もしかしたら僕が私利私欲のために使うかもしれないです。

注目の投稿

 PythonのTweepyを利用して、Twitter APIを利用している。 その中で、ハマったポイントをメモしておく。 まず、Searchに関して。 Twitter検索は、クライアントアプリ側では、全期間の検索が可能になっている。 一方で、APIを利用する際は、過去1週間しか...