Einsteinってどんなもの?Heroku+PredictionIOを使って機械学習をわかった気になってみよう!(第3回)

はじめに

みなさん、こんにちは。

第1回ではHerokuにPredictionIOの実行環境、第2回ではSalesforceからデータを一括取得してEvent Serverにインポートする機能およびEngineのトレーニングまでを行いました。第3回となる今回は最終的な目的である、機械学習によるリコメンド機能をSalesforceに組み込んでみたいと思います。

Visualforce + Apexの開発

作成するページは、お気に入りのゲームを表示している状態でRecommendボタンをクリックすると、お勧めの商品をEngineから取得して表示するものです。

作成するVisualforceページ&Apexクラス

SimilarGame.page ゲームのリコメンドを表示するページ
SimilarGameController.cls SimilarGame.pegeのコントローラー
RecommendItemScoreDto.cls Engineからのリコメンド情報を保持する

最初にEngineからのリコメンド情報を保持するためのクラスを作成します。

ソース:RecommendItemSocreDto.cls

/**
 * Enginが返すRecommend情報を保持するDTOクラスです
 */
public with sharing class RecommendItemScoreDto {
	public ItemScore[] itemScores {get; set;}
	public static RecommendItemScoreDto parse(String json){
		return (RecommendItemScoreDto)System.JSON.deserialize(json, RecommendItemScoreDto.class);
	}
	public class ItemScore{
		public String item {get; set;}
		public Double score {get; set;}
	}
}

EngineからのリコメンドはJsonで返されます。deserializeしてitem(ゲーム)ごとのscore(スコア)を複数件保持します。

続いてコントローラーを作成します。

ソースコード:SimilarGameController.cls

/**
 * ゲームのお勧めを行います
 */
public with sharing class SimilarGameController {

	private static final String END_POINT = 'http://<YOUR_ENGINE>.herokuapp.com/queries.json';
	private static final Integer NUMBER_OF_RECOMMENDATION = 4;

	public Favorite__c favorite {get; set;}
	private ApexPages.StandardController stdController;

	public String request {get; set;}
	public String response {get; set;}
	public List<SimilarGameDto> similarGameDtos {get; set;}

	/**
	 * コンストラクタ
	 * @param stdController
	 * @return SimilarGameController
	 */
    public SimilarGameController(ApexPages.StandardController stdController) {
        this.favorite = (Favorite__c)stdController.getRecord();
		this.favorite = [Select Id, Name, Game__c, GameNumber__c, Customer__c, CustomerNumber__c, Image__c, Genre__c From Favorite__c Where Id = :this.favorite.Id];
		this.stdController = stdController;
    }

	/**
	 * Cancelボタンのハンドラです
	 * @param
	 * @return PageReference
	 */
	public PageReference doCancel(){
		return stdController.cancel();
	}

	/**
	 * Recommendボタンのハンドラです
	 * @param
	 * @return PageReference
	 */
	public PageReference doRecommend(){
		createDto(getRecommend());
		return null;
	}

	/**
	 * EngineからRecommendを取得します
	 * @param
	 * @return RecommendItemScoreDto
	 */
	private RecommendItemScoreDto getRecommend(){
		JSONGenerator generator = JSON.createGenerator(true);
		generator.writeStartObject();
		generator.writeFieldName('items');
		generator.writeStartArray();
		generator.writeString(favorite.GameNumber__c);
		generator.writeEndArray();
		generator.writeNumberField('num', NUMBER_OF_RECOMMENDATION);
		generator.writeEndObject();
		request = generator.getAsString();

		HttpRequest req = new HttpRequest();
		req.setHeader('Content-type', 'application/json');
		req.setMethod('GET');
		req.setEndPoint(END_POINT);
		req.setBody(request);
		req.setTimeout(60000);

		RecommendItemScoreDto itemScoreDto = null;
		try{
			Http http = new Http();
			HttpResponse res = http.send(req);
			response = res.getBody();
			itemScoreDto = RecommendItemScoreDto.parse(response);
		}catch(Exception e){
			System.debug(LoggingLevel.ERROR, e);
		}
		return itemScoreDto;
	}

	/**
	 * 画面表示用のDTOを生成します
	 * @param
	 * @return
	 */
	private void createDto(RecommendItemScoreDto itemScoreDto){
		similarGameDtos = new List<SimilarGameDto>();

		Map<String, SimilarGameDto> similarGameMap = new Map<String, SimilarGameDto>();
		for(RecommendItemScoreDto.ItemScore itemScore : itemScoreDto.itemScores){
			SimilarGameDto similarGameDto = new SimilarGameDto();
			similarGameDto.gameNumber = itemScore.item;
			similarGameDto.score = itemScore.score;
			similarGameMap.put(similarGameDto.gameNumber, similarGameDto);
			similarGameDtos.add(similarGameDto);
		}

		for(Game__c game : [Select Name, GameNumber__c, Genre__c, ImageURL__c From Game__c Where GameNumber__c In : similarGameMap.keySet()]){
			SimilarGameDto similarGameDto = similarGameMap.get(game.GameNUmber__c);
			similarGameDto.game = game.Name;
			similarGameDto.genre = game.Genre__c;
			similarGameDto.image = game.ImageURL__c;
		}
	}

	/**
	 * Recommend情報インナークラス
	 */
	public class SimilarGameDto{
		public String game {get; set;}
		public String gameNumber {get; set;}
		public String image {get; set;}
		public String genre {get; set;}
		public Double score {get; set;}
	}
}

6行目の「http://<YOUR_ENGINE>/queries.json」はEngineの名称です。Event Serverではない点に注意しましょう。(例. http://pio-engine-jimaoka.herokuapp.com)

Engineからのリコメンドの取得は、getRecommend()メソッドで行っています。リクエストするJsonを組み立て、Calloutを使用してEngineにリクエストを送信しています。

Engineに対するリクエストは以下のようなJsonになります。

{ "items": ["G-000003"], "num": 4 }

Engineからのレスポンスは以下のようなJsonになります。

{"itemScores":
  [
    { "item":"G-000005","score":0.5505472425881496 },
    { "item":"G-000001","score":0.3845852702645052 }
  ]
}

createDto()メソッドでは、getRecommend()メソッドで取得したリコメンド情報を画面で表示する際にゲーム名やジャンル等、Salesforce内のデータと関連付けて、画面表示用データを作成しています。

最後にVisualforceページの作成です。

ソースコード:SimilarGame.page

<apex:page showHeader="true" sidebar="true" StandardController="Favorite__c" extensions="SimilarGameController">
	<apex:form id="fm">
		<apex:sectionHeader title="お気に入りからリコメンドするデモ" subtitle="{!favorite.Name}" />
		<apex:pageBlock id="pb">
			<apex:pageBlockButtons id="pbb">
				<apex:commandBUtton value="Cancel" action="{!doCancel}" />
				<apex:commandBUtton value="Recommend" action="{!doRecommend}" rerender="opResult" />
			</apex:pageBLockButtons>
			<apex:pageBlockSection columns="2">
				<apex:outputField value="{!favorite.Game__c}" />
				<apex:outputField value="{!favorite.Customer__c}" />
				<apex:outputField value="{!favorite.Genre__c}" />
			</apex:pageBlockSection>
		</apex:pageBLock>
		<apex:outputPanel id="opResult">
			<apex:pageBlock id="pbResult" title="おすすめ商品">
				<table width="100%">
					<tr>
						<apex:repeat value="{!similarGameDtos}" var="dto">
						<td>
							<apex:outputText value="{!dto.game}" />
							<br/>
							<apex:outputText value="{!dto.genre}" />
							<br/>
							<apex:outputText value="{!dto.score}" />
							<br/>
							<img src="{!dto.image}" height="80" width="80" />
						</td>
						</apex:repeat>
					</tr>
				</table>
			</apex:pageBlock>
			<apex:pageBlock id="pbReqRes" title="Request / Response">
				<apex:pageBlockSection columns="2">
					<apex:pageBlockSectionItem>
						<apex:outputLabel value="Request" />
						<apex:inputTextarea rows="10" cols="50" value="{!request}" disabled="true" />
					</apex:pageBlockSectionItem>
					<apex:pageBlockSectionItem>
						<apex:outputLabel value="Response" />
						<apex:inputTextarea rows="10" cols="50" value="{!response}" disabled="true" />
					</apex:pageBlockSectionItem>
				</apex:pageBlockSection>
			</apex:pageBlock>
		</apex:outputPanel>
	</apex:form>
</apex:page>

Salesforceのカスタマイズ

リモートサイトの設定

Engineに対してCalloutを行うためリモートサイトを設定します。

リモートサイト名

<YOUR_ENGINE_NAME>(例. pio-engine-jimaoka)

リモートサイト

http://<YOUR_ENGINE_NAME>.herokuapp.com (例. http://pio-engine-jimaoka.herokuapp.com)

カスタムボタンの追加

お気入りオブジェクトの詳細ページのカスタムボタンから、作成したVisualforceページを呼び出すようにカスタムボタンを追加します。

動作確認

すべての設定が完了しましたので、動作確認を行います。

期待通りに動作すると、図のようにリコメンドされたゲームが表示されます。

おわりに

3回に渡ってHeroku+PredictionIOとSalesforceを連携させるサンプルシステムを作ってみましたが如何でしたでしょうか?Einsteinの発表以前からIBMのWatsonをはじめ、Google、Microsoft、Amazonといったクラウドベンダーは機械学習の実行環境やAPIを提供していました。機械学習の注目が高まるにつれ、気になっていた開発者の方も多いと思います。SalesforceでもHerokuを使って簡単に実行環境を手にすることができます。実際に機械学習に触れてみたらどうでしょう?

すべてのソースコード・サンプルデータのリポジトリ