【書評】『AIとコミュニケーションする技術』 なんとなく使っていたAIの"解像度"を爆上げしてくれた一冊
はじめに
毎日開発業務で生成AIを使用していますが、AIについて断片的な知識しか持っておらず、網羅的に正確な情報をインプットしなければと、危機感を持ったのでこちらの本を読みました。
本の感想
この本は全部で以下の章構成になっていました。
- prologue ► 生成AIの現在地図
- chapter 1 ► 40のキーワードでひもとく生成 AI
- chapter 2 ► 生成 AI に伝わるプロンプトの書き方
- chapter 3 ► 生成 AI のポテンシャルを引き出すプロンプトの使い方
- chapter 4 ► プロンプトエンジニアリングの基礎
- chapter 5 ► 生成 AI のビジネス活用ナレッジ
- chapter 6 ► 進化し続けるテクノロジーと AI リテラシー
以下各chapter毎の感想を簡単にまとめます。
prologue ► 生成AIの現在地図
AIの起源、これまでのAIブームの解説がありました。AIの概念自体は1940年代後半に誕生したと知って大変驚きました。その後数回のブームが訪れるわけですが、まさに今、これまでのブームを遥かに凌駕する革命的ブームが訪れており、時代の転換点にまさしく生きているのだと実感しました。
chapter 1 ► 40のキーワードでひもとく生成 AI
AIに関連する40の重要なキーワードを解説するチャプターでした。端的に非常にためになりました。最初の方は基礎的で知っているキーワードもあったのですが、正直半分以上は知らない、もしくは聞いたことがあるかどうかレベルの単語でした。
これらのキーワードは今後ますます前提知識になると思いますので、知っているキーワードは改めて正確に復習ができましたし、知らないキーワードはインプットできて良かったです。
chapter 2 ► 生成 AI に伝わるプロンプトの書き方
AIに渡す指示(プロンプト)をデザインする手法を学べるチャプターでした。プロンプトでは一貫性のある言葉を使うことの重要性を知りました。なんとなく生成AIを使用していると、同じ物を指しているのに次の指示では別の言葉を使ってしまったりしているな、と普段の自分の使い方を振り返りました。
また、目的を明確に説明するというのも、基本的なテクニックではありますが、自分は何を知りたいからこの依頼をしているのか、指示する前に一瞬立ち止まって、目的を明瞭に伝えているか再考することで、「よい回答」を引き出せる確率が上がると思いました。
また、やっぱり英語でやり取りすると日本語に翻訳しないぶん、大幅にトークンを節約できるのですね…英語だと自分の情報処理に時間がかかってしまうので日本語でやり取りをしていますが、時間に余裕がある時は勉強も兼ねて積極的に英語を使いたいです。
chapter 3 ► 生成 AI のポテンシャルを引き出すプロンプトの使い方
生成AIの主要スキル、活用方法の大きな軸についてまとまっていて良かったです。生成AIは何が得意でどうビジネスに活かすことができるのか、改めて整理することができました。人格の再現は自分ではあまりやったことがありませんでした。
AIに特定の人格に成りきらせるのは、面接練習などに活用できそうで面白いなと思いました。
chapter 4 ► プロンプトエンジニアリングの基礎
プロンプトエンジニアリングの実践的な構文や手法を学ぶことができて非常にためになりました。例えば文章要約のタスクを一回の指示で一気に行わせるのではなく、まず文章中の重要と考えられるキーワードを抽出させてから、それを利用して要約をさせると精度が良くなるとありました。(方向性刺激プロンプティング)
ひとつステップを踏ませたり、少数の例示を複数提供したり、こちらの手間をひとつ増やすだけで回答精度が良くなるということは前提知識として頭に入れておこうと改めて思いました。
chapter 5 ► 生成 AI のビジネス活用ナレッジ
ビジネスに応用するうえで、前提となる概念や生成AIの正確な理解を解説したチャプターでした。AIモデルの選定や生成結果の評価手法、さらにセキュリティリスクや生成物の著作権等の法的な説明もあって良かったです。
便利な生成AIを今後安全に使用していくためのナレッジ、リテラシーがまとまっていて勉強になりました。
chapter 6 ► 進化し続けるテクノロジーと AI リテラシー
生成AIを活用していくためのユーザー自身のマインドセットやスキルを解説したチャプターでした。エンジニアへの影響については、生成AIの力によってプロジェクトのリリースが短縮されることになれば、次の現場で新たな体験をするサイクルが早くなる可能性があるとありました。
確かに、エンジニアにとっては、ワクワクできて成長もできる開発体験をどれだけ積めるかはキャリア形成のために重要だと思うので、その観点はとても腹落ちしました。
まとめ
ネットには生成AIに関する情報が散乱していますが、逆に体系的に情報をインプットすることが難しいとも言えると思います。この本はAIの基礎知識から具体的なプロンプトテクニック、未来への影響の考察まで幅広く情報が網羅されており、まさに自分が求めていた本でした。
なんとなくわかったような気になっていた生成AIですが、格段に解像度が上がったと思います。週末にサクッと読めるくらいの分量であるのも個人的にはかなり評価が高かったです。読み返しながら日常の業務に活かしていきたいと思います。
AtCoder Beginner Contest 411 B - Distance Table
AtCoder Beginner Contest 411 B問題
累積和を使用した回答
#!/usr/bin/env ruby n = gets.to_i d = gets.split.map(&:to_i) total = Array.new(n, 0) # 駅間の距離の累積和の配列 (0..n-2).each do |i| total[i+1] += total[i] + d[i] end # 駅i,駅jの距離はtotal[j]-total[i] (0..n-2).each do |i| ans = [] (i+1..n-1).each do |j| ans << total[j] - total[i] end puts ans.join(" ") end
解法のポイント
- 駅間の距離の累積和を配列で保持(total)
- 駅i,駅jの距離はtotal[j] - total[i]によって求めることができる
- 計算量は累積和の作成O(n) + 出力O(n2) = O(n2)。本問題の制約においてはAC可能。
AtCoder Beginner Contest 410 C - Rotatable Array
AtCoder Beginner Contest 410 結果
AB2完、Cは計算量削減の方法の検討がつかずTLE撃沈でした。 パフォーマンス177、新レーティング326(△21) ここ4週連続でレートが転げ落ちています。。 AB問題の解くスピードが遅いこと、C問題を安定して解く実力がまだないことが伸び悩んでいる原因です。 単純に特に最近の練習量が少なすぎるので当然の結果だと思います。 rあらためてコンテストの復習と日頃の練習をこれから頑張ります。
AtCoder Beginner Contest 410 C問題
atcoder.jp 自分の実装ではrotateメソッドで配列を操作していたため、type3の計算量はO(n)となりTLEとなりました。
回答例のように先頭からどれだけずれているか保持する変数を持つことで、type3における実際の配列操作が不要になり、大幅に計算量を削減することができます。
回答例
#!/usr/bin/env ruby n, q = gets.split.map(&:to_i) arr = (1..n).to_a offset = 0 # type3によるズレを記録する q.times do type, val1, val2 = gets.split.map(&:to_i) case type when 1 p, x = val1, val2 p -= 1 real_index = (offset + p) % n arr[real_index] = x when 2 p = val1 p -= 1 real_index = (offset + p) % n puts arr[real_index] when 3 k = val1 offset = (offset + k) % n end end
Ruby Array.newを整理しました
はじめに
AtCoderで配列を用意するときにArray.newをよく使用するのですが、同一オブジェクトを参照する挙動やブロックの記法などが曖昧になってしまっているので、自分用に記憶定着のためアウトプットします。
記法
Array.new(要素数、オブジェクト)と書くと、配列の全要素が同一のオブジェクトを参照する。
irb(main):024> ary = Array.new(3, "hoge") => ["hoge", "hoge", "hoge"] irb(main):025> ary => ["hoge", "hoge", "hoge"] irb(main):026> ary[0].capitalize! => "Hoge" irb(main):027> ary => ["Hoge", "Hoge", "Hoge"] # 0番目を指定したはずが全要素大文字になった irb(main):028> ary.map(&:object_id) => [126728, 126728, 126728] # 要素が全て同一のオブジェクト
irb(main):019> ary = Array.new(3, [0]) => [[0], [0], [0]] irb(main):020> ary => [[0], [0], [0]] irb(main):021> ary [0][0] = 3 => 3 irb(main):022> ary => [[3], [3], [3]] # 全ての要素が3に書き変わってしまった。 irb(main):023> ary.map(&:object_id) => [98128, 98128, 98128] # 要素が全て同一のオブジェクト
Array.new(要素数) { ... }
同一オブジェクトを参照して欲しくない場合、解決策としてはブロックを使います。
irb(main):029> ary = Array.new(3){ "hoge" } => ["hoge", "hoge", "hoge"] irb(main):030> ary => ["hoge", "hoge", "hoge"] irb(main):031> ary[0].capitalize! => "Hoge" irb(main):032> ary => ["Hoge", "hoge", "hoge"] # 指定した要素のみ大文字になった irb(main):033> ary.map(&:object_id) => [139784, 139792, 139800] # 全要素が異なるオブジェクトID
irb(main):036> ary = Array.new(3){ [0] } => [[0], [0], [0]] irb(main):037> ary[0][0] = 3 => 3 irb(main):038> ary => [[3], [0], [0]] # 指定した要素のみ3に変わった irb(main):039> ary.map(&:object_id) => [170616, 170624, 170632] # 全要素が異なるオブジェクトID
上記のようにブロックを使うことで新しいオブジェクトを3つ作ることができます。
Array.new(要素数){ |index| ... }
ブロックの引数にインデックスを取ることができます。
irb(main):042> ary = Array.new(5){ |index| [0, index] } => [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]] irb(main):043> ary => [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]]
AtCoderでよくありますが、配列の0番目にカウント、1番目に添え字を持った配列を用意したいときなどに便利な記法です。
AtCoder Beginner Contest407振り返り
ABに30分時間を消費しCも計算量見積もりが甘く今回は全くダメでした。 ABは10分程度で解けないといけない問題だったと思います。 最近練習量が落ちていることが確実に影響していますね、、気合を入れ直したいと思います。 パフォーマンス248、新Rating363(△15)。
C問題 解答例
atcoder.jp 解答方針は考察できましたが、計算量見積もりが不完全で実装ミスもありました。 自分の弱い部分がわかったので反省して次に活かします。
# !/usr/bin/env ruby s = gets.chomp ans = 0 # ボタン操作の総回数 x = 0 # Bボタン(デクリメント回数)の合計 while !s.empty? # sが空でない限り続ける loop do d = s[-1].to_i # 最後の文字をintに変換 if (d - x) % 10 == 0 break end # 末尾の文字が0でなければボタンBを押す x += 1 ans += 1 end # dが0の場合削除して、操作回数を+1する s.chop! ans += 1 end puts ans
AtCoder Beginner Contest406振り返り
C問題撃沈。 難易度が高めだったようでレーティングは上がりました。 パフォーマンス572、新Rating378(+22)。
C問題 ランレングスエンコーディングを用いた回答
atcoder.jp チルダ型になる条件を考察することはできましたが実装に至りませんでした。 ランレングスエンコーディングという手法を初めて知りました。
#!/usr/bin/env ruby n = gets.to_i p = gets.split.map(&:to_i) # p[i] < p[i+1] なら 0、そうでなければ 1の配列に変換 d = [] (0...(n - 1)).each do |i| d.push((p[i] < p[i + 1]) ? 0 : 1) end # ランレングスエンコーディング (rle) rle = [] # [文字の種類、何連続続くか]のペアの配列 d.each do |x| if !rle.empty? && rle.last[0] == x rle.last[1] += 1 else rle.push([x, 1]) end end # chunkメソッド使用の場合 # rle = d.chunk { |x| x }.map { |val, arr| [val, arr.size]} ans = 0 (0..rle.size-2).each do |i| if rle[i][0] == 1 l = 0 # 直前の0の連続数 r = 0 # 直後の0の連続数 if i > 0 l = rle[i-1][1] end r = rle[i+1][1] ans += l * r end end puts ans
AtCoder Beginner Contest405振り返り
ABC3完答でしたがC問題に時間がかかり低調なパフォーマンスでした。 パフォーマンス287、新Rating356(△8)。
C問題 提出コード
普通に実装するとO(N2)になるので配列の合計値を利用することで計算量を削減する実装を思いつくことができました。 思いついたのはいいのですが思考に時間がかかりすぎました。
#!/usr/bin/env ruby n = gets.to_i a = gets.split.map(&:to_i) sum = a.sum ans = 0 (0..n-2).each do |i| sum -= a[i] ans += a[i] * sum end puts ans
C問題 公式解説動画の別解
正方形のマスで考えて配列の合計値の2乗-対角線/2が答えになる。 この考え方は全く思いつかなかったので今後に活かすために覚えておきたいです。
#!/usr/bin/env ruby n = gets.to_i a = gets.split.map(&:to_i) sum = a.sum sum2 = a.map{ |n| n ** 2 }.sum ans = (sum ** 2 - sum2) / 2 puts ans