Class: Transformers::QuestionAnsweringPipeline

Inherits:
ChunkPipeline show all
Extended by:
ClassAttribute
Defined in:
lib/transformers/pipelines/question_answering.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ClassAttribute

class_attribute

Methods inherited from ChunkPipeline

#run_single

Methods inherited from Pipeline

#_ensure_tensor_on_device, #check_model_type, #get_iterator, #torch_dtype

Constructor Details

#initialize(model, tokenizer:, modelcard: nil, framework: nil, task: "", **kwargs) ⇒ QuestionAnsweringPipeline

Returns a new instance of QuestionAnsweringPipeline.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/transformers/pipelines/question_answering.rb', line 74

def initialize(
  model,
  tokenizer:,
  modelcard: nil,
  framework: nil,
  task: "",
  **kwargs
)
  super(
    model,
    tokenizer: tokenizer,
    modelcard: modelcard,
    framework: framework,
    task: task,
    **kwargs
  )

  @args_parser = QuestionAnsweringArgumentHandler.new
  check_model_type(MODEL_FOR_QUESTION_ANSWERING_MAPPING_NAMES)
end

Class Method Details

.create_sample(question:, context:) ⇒ Object



95
96
97
98
99
100
101
102
103
# File 'lib/transformers/pipelines/question_answering.rb', line 95

def self.create_sample(
  question:, context:
)
  if question.is_a?(Array)
    question.zip(context).map { |q, c| SquadExample.new(nil, q, c, nil, nil, nil) }
  else
    SquadExample.new(nil, question, context, nil, nil, nil)
  end
end

Instance Method Details

#_forward(inputs) ⇒ Object



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/transformers/pipelines/question_answering.rb', line 297

def _forward(inputs)
  example = inputs[:example]
  model_inputs = @tokenizer.model_input_names.to_h { |k| [k.to_sym, inputs[k.to_sym]] }
  # `XXXForSequenceClassification` models should not use `use_cache=True` even if it's supported
  # model_forward = @model.forward if self.framework == "pt" else self.model.call
  # if "use_cache" in inspect.signature(model_forward).parameters.keys():
  #   model_inputs[:use_cache] = false
  # end
  output = @model.(**model_inputs)
  if output.is_a?(Hash)
    {start: output[:start_logits], end: output[:end_logits], example: example}.merge(inputs)
  else
    start, end_ = output[...2]
    {start: start, end: end_, example: example}.merge(inputs)
  end
end

#_sanitize_parameters(padding: nil, topk: nil, top_k: nil, doc_stride: nil, max_answer_len: nil, max_seq_len: nil, max_question_len: nil, handle_impossible_answer: nil, align_to_words: nil, **kwargs) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/transformers/pipelines/question_answering.rb', line 105

def _sanitize_parameters(
  padding: nil,
  topk: nil,
  top_k: nil,
  doc_stride: nil,
  max_answer_len: nil,
  max_seq_len: nil,
  max_question_len: nil,
  handle_impossible_answer: nil,
  align_to_words: nil,
  **kwargs
)
  # Set defaults values
  preprocess_params = {}
  if !padding.nil?
    preprocess_params[:padding] = padding
  end
  if !doc_stride.nil?
    preprocess_params[:doc_stride] = doc_stride
  end
  if !max_question_len.nil?
    preprocess_params[:max_question_len] = max_question_len
  end
  if !max_seq_len.nil?
    preprocess_params[:max_seq_len] = max_seq_len
  end

  postprocess_params = {}
  if !topk.nil? && top_k.nil?
    warn("topk parameter is deprecated, use top_k instead")
    top_k = topk
  end
  if !top_k.nil?
    if top_k < 1
      raise ArgumentError, "top_k parameter should be >= 1 (got #{top_k})"
    end
    postprocess_params[:top_k] = top_k
  end
  if !max_answer_len.nil?
    if max_answer_len < 1
      raise ArgumentError, "max_answer_len parameter should be >= 1 (got #{max_answer_len})"
    end
  end
  if !max_answer_len.nil?
    postprocess_params[:max_answer_len] = max_answer_len
  end
  if !handle_impossible_answer.nil?
    postprocess_params[:handle_impossible_answer] = handle_impossible_answer
  end
  if !align_to_words.nil?
    postprocess_params[:align_to_words] = align_to_words
  end
  [preprocess_params, {}, postprocess_params]
end

#call(*args, **kwargs) ⇒ Object



160
161
162
163
164
165
166
# File 'lib/transformers/pipelines/question_answering.rb', line 160

def call(*args, **kwargs)
  examples = @args_parser.(*args, **kwargs)
  if examples.is_a?(Array) && examples.length == 1
    return super(examples[0], **kwargs)
  end
  super(examples, **kwargs)
end

#decode_spans(start, end_, topk, max_answer_len, undesired_tokens) ⇒ Object



411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/transformers/pipelines/question_answering.rb', line 411

def decode_spans(
  start, end_, topk, max_answer_len, undesired_tokens
)
  # Ensure we have batch axis
  if start.ndim == 1
    start = start[nil]
  end

  if end_.ndim == 1
    end_ = end_[nil]
  end

  # Compute the score of each tuple(start, end) to be the real answer
  outer = start.expand_dims(-1).dot(end_.expand_dims(1))

  # Remove candidate with end < start and end - start > max_answer_len
  candidates = outer.triu.tril(max_answer_len - 1)

  # Inspired by Chen & al. (https://github.com/facebookresearch/DrQA)
  scores_flat = candidates.flatten
  if topk == 1
    idx_sort = [scores_flat.argmax]
  elsif scores_flat.length < topk
    raise Todo
  else
    raise Todo
  end

  starts, ends = unravel_index(idx_sort, candidates.shape)[1..]
  desired_spans = isin(starts, undesired_tokens.where) & isin(ends, undesired_tokens.where)
  starts = starts[desired_spans]
  ends = ends[desired_spans]
  scores = candidates[0, starts, ends]

  [starts, ends, scores]
end

#get_indices(enc, s, e, sequence_index, align_to_words) ⇒ Object



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/transformers/pipelines/question_answering.rb', line 388

def get_indices(
  enc, s, e, sequence_index, align_to_words
)
  if align_to_words
    begin
      start_word = enc.token_to_word(s)
      end_word = enc.token_to_word(e)
      start_index = enc.word_to_chars(start_word, sequence_index)[0]
      end_index = enc.word_to_chars(end_word, sequence_index)[1]
    rescue
      # TODO
      raise
      # Some tokenizers don't really handle words. Keep to offsets then.
      start_index = enc.offsets[s][0]
      end_index = enc.offsets[e][1]
    end
  else
    start_index = enc.offsets[s][0]
    end_index = enc.offsets[e][1]
  end
  [start_index, end_index]
end

#isin(element, test_elements) ⇒ Object



459
460
461
462
# File 'lib/transformers/pipelines/question_answering.rb', line 459

def isin(element, test_elements)
  test_elements = test_elements.to_a
  Numo::Bit.cast(element.to_a.map { |e| test_elements.include?(e) })
end

#postprocess(model_outputs, top_k: 1, handle_impossible_answer: false, max_answer_len: 15, align_to_words: true) ⇒ Object



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/transformers/pipelines/question_answering.rb', line 314

def postprocess(
  model_outputs,
  top_k: 1,
  handle_impossible_answer: false,
  max_answer_len: 15,
  align_to_words: true
)
  min_null_score = 1000000  # large and positive
  answers = []
  model_outputs.each do |output|
    start_ = output[:start]
    end_ = output[:end]
    example = output[:example]
    p_mask = output[:p_mask]
    attention_mask = (
      !output[:attention_mask].nil? ? output[:attention_mask].numo : nil
    )

    starts, ends, scores, min_null_score = select_starts_ends(
      start_, end_, p_mask, attention_mask, min_null_score, top_k, handle_impossible_answer, max_answer_len
    )

    if !@tokenizer.is_fast
      raise Todo
    else
      # Convert the answer (tokens) back to the original text
      # Score: score from the model
      # Start: Index of the first character of the answer in the context string
      # End: Index of the character following the last character of the answer in the context string
      # Answer: Plain text of the answer
      question_first = @tokenizer.padding_side == "right"
      enc = output[:encoding]

      # Encoding was *not* padded, input_ids *might*.
      # It doesn't make a difference unless we're padding on
      # the left hand side, since now we have different offsets
      # everywhere.
      if @tokenizer.padding_side == "left"
        offset = output[:input_ids].eq(@tokenizer.pad_token_id).numo.sum
      else
        offset = 0
      end

      # Sometimes the max probability token is in the middle of a word so:
      # - we start by finding the right word containing the token with `token_to_word`
      # - then we convert this word in a character span with `word_to_chars`
      sequence_index = question_first ? 1 : 0
      starts.to_a.zip(ends.to_a, scores.to_a) do |s, e, score|
        s = s - offset
        e = e - offset

        start_index, end_index = get_indices(enc, s, e, sequence_index, align_to_words)

        answers <<
          {
            score: score[0],
            start: start_index,
            end: end_index,
            answer: example.context_text[start_index...end_index]
          }
      end
    end
  end

  if handle_impossible_answer
    answers << {score: min_null_score, start: 0, end: 0, answer: ""}
  end
  answers = answers.sort_by { |x| -x[:score] }[...top_k]
  if answers.length == 1
    return answers[0]
  end
  answers
end

#preprocess(example, padding: "do_not_pad", doc_stride: nil, max_question_len: 64, max_seq_len: nil) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/transformers/pipelines/question_answering.rb', line 168

def preprocess(example, padding: "do_not_pad", doc_stride: nil, max_question_len: 64, max_seq_len: nil)
  # XXX: This is specal, args_parser will not handle anything generator or dataset like
  # For those we expect user to send a simple valid example either directly as a SquadExample or simple dict.
  # So we still need a little sanitation here.
  if example.is_a?(Hash)
    example = SquadExample.new(nil, example[:question], example[:context], nil, nil, nil)
  end

  if max_seq_len.nil?
    max_seq_len = [@tokenizer.model_max_length, 384].min
  end
  if doc_stride.nil?
    doc_stride = [max_seq_len.div(2), 128].min
  end

  if doc_stride > max_seq_len
    raise ArgumentError, "`doc_stride` (#{doc_stride}) is larger than `max_seq_len` (#{max_seq_len})"
  end

  if !@tokenizer.is_fast
    features = squad_convert_examples_to_features(
      examples: [example],
      tokenizer: @tokenizer,
      max_seq_length: max_seq_len,
      doc_stride: doc_stride,
      max_query_length: max_question_len,
      padding_strategy: PaddingStrategy::MAX_LENGTH,
      is_training: false,
      tqdm_enabled: false
    )
  else
    # Define the side we want to truncate / pad and the text/pair sorting
    question_first = @tokenizer.padding_side == "right"

    encoded_inputs = @tokenizer.(
      question_first ? example.question_text : example.context_text,
      text_pair: question_first ? example.context_text : example.question_text,
      padding: padding,
      truncation: question_first ? "only_second" : "only_first",
      max_length: max_seq_len,
      stride: doc_stride,
      return_token_type_ids: true,
      return_overflowing_tokens: true,
      return_offsets_mapping: true,
      return_special_tokens_mask: true\
    )
    # When the input is too long, it's converted in a batch of inputs with overflowing tokens
    # and a stride of overlap between the inputs. If a batch of inputs is given, a special output
    # "overflow_to_sample_mapping" indicate which member of the encoded batch belong to which original batch sample.
    # Here we tokenize examples one-by-one so we don't need to use "overflow_to_sample_mapping".
    # "num_span" is the number of output samples generated from the overflowing tokens.
    num_spans = encoded_inputs[:input_ids].length

    # p_mask: mask with 1 for token than cannot be in the answer (0 for token which can be in an answer)
    # We put 0 on the tokens from the context and 1 everywhere else (question and special tokens)
    p_mask =
      num_spans.times.map do |span_id|
        encoded_inputs.sequence_ids(span_id).map { |tok| tok != (question_first ? 1 : 0) }
      end

    features = []
    num_spans.times do |span_idx|
      input_ids_span_idx = encoded_inputs[:input_ids][span_idx]
      attention_mask_span_idx = (
        encoded_inputs.include?(:attention_mask) ? encoded_inputs[:attention_mask][span_idx] : nil
      )
      token_type_ids_span_idx = (
        encoded_inputs.include?(:token_type_ids) ? encoded_inputs[:token_type_ids][span_idx] : nil
      )
      # keep the cls_token unmasked (some models use it to indicate unanswerable questions)
      if !@tokenizer.cls_token_id.nil?
        cls_indices = (Numo::NArray.cast(input_ids_span_idx).eq(@tokenizer.cls_token_id)).where
        cls_indices.each do |cls_index|
          p_mask[span_idx][cls_index] = false
        end
      end
      submask = p_mask[span_idx]
      features <<
        SquadFeatures.new(
          input_ids: input_ids_span_idx,
          attention_mask: attention_mask_span_idx,
          token_type_ids: token_type_ids_span_idx,
          p_mask: submask,
          encoding: encoded_inputs[span_idx],
          # We don't use the rest of the values - and actually
          # for Fast tokenizer we could totally avoid using SquadFeatures and SquadExample
          cls_index: nil,
          token_to_orig_map: {},
          example_index: 0,
          unique_id: 0,
          paragraph_len: 0,
          token_is_max_context: 0,
          tokens: [],
          start_position: 0,
          end_position: 0,
          is_impossible: false,
          qas_id: nil
        )
    end
  end

  features.each_with_index do |feature, i|
    fw_args = {}
    others = {}
    model_input_names = @tokenizer.model_input_names + ["p_mask", "token_type_ids"]

    feature.instance_variables.each do |k|
      v = feature.instance_variable_get(k)
      k = k[1..]
      if model_input_names.include?(k)
        if @framework == "tf"
          raise Todo
        elsif @framework == "pt"
          tensor = Torch.tensor(v)
          if tensor.dtype == Torch.int32
            tensor = tensor.long
          end
          fw_args[k.to_sym] = tensor.unsqueeze(0)
        end
      else
        others[k.to_sym] = v
      end
    end

    is_last = i == features.length - 1
    yield({example: example, is_last: is_last}.merge(fw_args).merge(others))
  end
end

#select_starts_ends(start, end_, p_mask, attention_mask, min_null_score = 1000000, top_k = 1, handle_impossible_answer = false, max_answer_len = 15) ⇒ Object



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/transformers/pipelines/question_answering.rb', line 464

def select_starts_ends(
  start,
  end_,
  p_mask,
  attention_mask,
  min_null_score = 1000000,
  top_k = 1,
  handle_impossible_answer = false,
  max_answer_len = 15
)
  # Ensure padded tokens & question tokens cannot belong to the set of candidate answers.
  undesired_tokens = ~p_mask.numo

  if !attention_mask.nil?
    undesired_tokens = undesired_tokens & attention_mask
  end

  # Generate mask
  undesired_tokens_mask = undesired_tokens.eq(0)

  # Make sure non-context indexes in the tensor cannot contribute to the softmax
  start = start.numo
  end_ = end_.numo
  start[undesired_tokens_mask] = -10000.0
  end_[undesired_tokens_mask] = -10000.0

  # Normalize logits and spans to retrieve the answer
  start = Numo::NMath.exp(start - start.max(axis: -1, keepdims: true))
  start = start / start.sum

  end_ = Numo::NMath.exp(end_ - end_.max(axis: -1, keepdims: true))
  end_ = end_ / end_.sum

  if handle_impossible_answer
    min_null_score = [min_null_score, (start[0, 0] * end_[0, 0]).item].min
  end

  # Mask CLS
  start[0, 0] = end_[0, 0] = 0.0

  starts, ends, scores = decode_spans(start, end_, top_k, max_answer_len, undesired_tokens)
  [starts, ends, scores, min_null_score]
end

#unravel_index(indices, shape) ⇒ Object



448
449
450
451
452
453
454
455
456
457
# File 'lib/transformers/pipelines/question_answering.rb', line 448

def unravel_index(indices, shape)
  indices = Numo::NArray.cast(indices)
  result = []
  factor = 1
  shape.size.times do |i|
    result.unshift(indices / factor % shape[-1 - i])
    factor *= shape[-1 - i]
  end
  result
end