このコミットが含まれているのは:
@@ -27,26 +27,20 @@ class GekanatorGamesController < ApplicationController
|
||||
game = GekanatorGame.find_by(id: params[:id])
|
||||
return head :not_found unless game
|
||||
|
||||
asked_ids = Array(game.answers).filter_map { |answer| answer_question_id(answer) }
|
||||
existing_example_ids =
|
||||
GekanatorQuestionExample.where(post_id: game.correct_post_id)
|
||||
.select(:gekanator_question_id)
|
||||
# Direct examples only for now; post_similarity-based expansion is deferred.
|
||||
questions =
|
||||
GekanatorQuestion
|
||||
.accepted
|
||||
.includes(:gekanator_question_examples)
|
||||
.where(kind: 'post_similarity', source: 'user_suggested')
|
||||
.where.not(id: existing_example_ids)
|
||||
.order(priority_weight: :desc, id: :asc)
|
||||
.to_a
|
||||
|
||||
selected = weighted_sample_questions(
|
||||
questions,
|
||||
post_id: game.correct_post_id,
|
||||
limit: 2)
|
||||
|
||||
render json: {
|
||||
questions: questions.filter_map { |question|
|
||||
json = extra_question_json(question)
|
||||
next if asked_ids.include?(json[:id].to_s)
|
||||
next if asked_ids.include?("post-similarity:#{ json[:id] }")
|
||||
|
||||
json
|
||||
}.first(2)
|
||||
questions: selected.map { |question| extra_question_json(question) }
|
||||
}
|
||||
end
|
||||
|
||||
@@ -84,11 +78,10 @@ class GekanatorGamesController < ApplicationController
|
||||
gekanator_question: question,
|
||||
post: game.correct_post,
|
||||
user: current_user)
|
||||
example.assign_attributes(
|
||||
gekanator_game: game,
|
||||
example.record_answer!(
|
||||
answer: item[:answer],
|
||||
source: 'post_game_extra',
|
||||
weight: 1.0)
|
||||
gekanator_game: game)
|
||||
example.save!
|
||||
end
|
||||
end
|
||||
@@ -107,12 +100,41 @@ class GekanatorGamesController < ApplicationController
|
||||
}
|
||||
end
|
||||
|
||||
def answer_question_id answer
|
||||
value = if answer.is_a?(Hash)
|
||||
answer['question_id'].presence || answer[:question_id].presence ||
|
||||
answer['questionId'].presence || answer[:questionId].presence
|
||||
def weighted_sample_questions questions, post_id:, limit:
|
||||
remaining = questions.uniq(&:id)
|
||||
selected = []
|
||||
|
||||
while selected.length < limit && remaining.any?
|
||||
weighted =
|
||||
remaining.map { |question|
|
||||
[question, selection_weight_for(question, post_id: post_id)]
|
||||
}
|
||||
total_weight = weighted.sum { |_question, weight| weight }
|
||||
break if total_weight <= 0
|
||||
|
||||
target = rand * total_weight
|
||||
cumulative = 0.0
|
||||
chosen =
|
||||
weighted.find do |_question, weight|
|
||||
cumulative += weight
|
||||
cumulative >= target
|
||||
end&.first || weighted.first.first
|
||||
|
||||
selected << chosen
|
||||
remaining.reject! { |question| question.id == chosen.id }
|
||||
end
|
||||
|
||||
value&.to_s
|
||||
selected
|
||||
end
|
||||
|
||||
def selection_weight_for question, post_id:
|
||||
sample_count =
|
||||
question.gekanator_question_examples.sum { |example|
|
||||
next 0 unless example.post_id == post_id
|
||||
|
||||
example.sample_count.presence || 1
|
||||
}
|
||||
|
||||
question.priority_weight.to_f / (1.0 + sample_count * 0.15)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class GekanatorQuestionExample < ApplicationRecord
|
||||
ANSWERS = GekanatorQuestionSuggestion::ANSWERS
|
||||
NON_UNKNOWN_ANSWERS = ANSWERS - ['unknown']
|
||||
SOURCES = ['initial_suggestion', 'post_game_extra'].freeze
|
||||
|
||||
belongs_to :gekanator_question
|
||||
@@ -8,10 +9,89 @@ class GekanatorQuestionExample < ApplicationRecord
|
||||
belongs_to :gekanator_game, optional: true
|
||||
|
||||
validates :answer, presence: true, inclusion: { in: ANSWERS }
|
||||
validates :answer_counts, presence: true
|
||||
validates :sample_count,
|
||||
presence: true,
|
||||
numericality: {
|
||||
only_integer: true,
|
||||
greater_than: 0
|
||||
}
|
||||
validates :source, presence: true, inclusion: { in: SOURCES }
|
||||
validates :weight,
|
||||
presence: true,
|
||||
numericality: {
|
||||
greater_than: 0
|
||||
}
|
||||
|
||||
before_validation :normalize_learning_state
|
||||
|
||||
def record_answer!(answer:, source:, gekanator_game: nil)
|
||||
answer = answer.to_s
|
||||
raise ArgumentError, 'invalid answer' unless ANSWERS.include?(answer)
|
||||
|
||||
counts = normalized_answer_counts
|
||||
counts[answer] += 1
|
||||
|
||||
self.answer_counts = counts
|
||||
self.sample_count = counts.values.sum
|
||||
self.gekanator_game = gekanator_game if gekanator_game.present?
|
||||
self.source = source if new_record?
|
||||
|
||||
apply_aggregated_answer!(preferred_answer: answer)
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_learning_state
|
||||
counts = normalized_answer_counts
|
||||
|
||||
if counts.values.sum.zero? && answer.present?
|
||||
counts[answer] = 1
|
||||
end
|
||||
|
||||
self.answer_counts = counts
|
||||
self.sample_count = counts.values.sum
|
||||
|
||||
apply_aggregated_answer!
|
||||
end
|
||||
|
||||
def apply_aggregated_answer!(preferred_answer: nil)
|
||||
counts = normalized_answer_counts
|
||||
known_counts = counts.slice(*NON_UNKNOWN_ANSWERS)
|
||||
known_total = known_counts.values.sum
|
||||
|
||||
if known_total.zero?
|
||||
self.answer = 'unknown'
|
||||
self.weight = 0.1
|
||||
return
|
||||
else
|
||||
max_count = known_counts.values.max
|
||||
candidates = known_counts.select { |_answer, count| count == max_count }.keys
|
||||
self.answer =
|
||||
if preferred_answer.present? && candidates.include?(preferred_answer)
|
||||
preferred_answer
|
||||
elsif answer.present? && candidates.include?(answer)
|
||||
answer
|
||||
else
|
||||
candidates.first
|
||||
end
|
||||
end
|
||||
|
||||
consensus = max_count.to_f / known_total
|
||||
self.weight = Math.sqrt(known_total) * consensus
|
||||
end
|
||||
|
||||
def normalized_answer_counts
|
||||
base = ANSWERS.index_with(0)
|
||||
|
||||
answer_counts.to_h.each do |key, value|
|
||||
answer_key = key.to_s
|
||||
next unless ANSWERS.include?(answer_key)
|
||||
|
||||
base[answer_key] = value.to_i
|
||||
end
|
||||
|
||||
base
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,13 +26,16 @@ module Gekanator
|
||||
},
|
||||
gekanator_question_suggestion: suggestion,
|
||||
created_by: user)
|
||||
GekanatorQuestionExample.create!(
|
||||
gekanator_question: question,
|
||||
post: suggestion.gekanator_game.correct_post,
|
||||
user: user,
|
||||
gekanator_game: suggestion.gekanator_game,
|
||||
example =
|
||||
GekanatorQuestionExample.new(
|
||||
gekanator_question: question,
|
||||
post: suggestion.gekanator_game.correct_post,
|
||||
user: user)
|
||||
example.record_answer!(
|
||||
answer: suggestion.answer,
|
||||
source: 'initial_suggestion')
|
||||
source: 'initial_suggestion',
|
||||
gekanator_game: suggestion.gekanator_game)
|
||||
example.save!
|
||||
suggestion.update!(processed: true)
|
||||
question
|
||||
end
|
||||
|
||||
新しい課題から参照
ユーザをブロックする