И я не говорю о псевдослучайности машин. Для большинства целей случайности компьютерной генерации достаточно. Я говорю о вашей, читательской, случайности. Ладно, не вы, а люди в целом.

Этот поиск начался, когда один-единственный вопрос занимал мою голову в течение недели. «Когда вы нажимаете на клавиатуру, насколько случайной получается строка?» Очевидно, что затирание не может быть целенаправленным, как печатание слов, и при этом оно не может быть усердным в выборе букв, которые не были нажаты. Просто вращайте руками вперед и назад по клавиатуре. Есть ли разница между вами, неуправляемым двуногим, нажимающим на кнопки, и электричеством и кремнием?

Я пытался выяснить это двумя способами: машинным обучением и эвристикой. И да, я вижу иронию в использовании машины, чтобы выяснить, производится ли что-то машиной. Еще одна причина, по которой я выбрал два метода, заключается в наблюдаемой в настоящее время заметной социальной тенденции, заключающейся в чрезмерном употреблении, а иногда (возможно) и ненужном использовании машинного обучения. Это не означает, что машинное обучение бесполезно, но иногда достаточно использовать эвристику, чтобы решить некоторые простые проблемы.

Чтобы решить эту проблему, мне нужно было получить некоторые базовые формы данных. Во-первых, я создал класс, который случайным образом генерировал текст, используя встроенную случайность Python. Простое создание целых чисел от 97 до 122 дает индекс Unicode строчных букв, а их объединение создает тип данных, который можно преобразовать в тензор.

Поскольку нам нужно сравнить машинную случайность с человеческой случайностью, я создал базовый класс, который принимает строковые входные данные, а затем разбивает их на M подстрок в массиве N/M. Причина, по которой я сделал это, заключалась в том, чтобы подготовить данные для ввода в алгоритм ML.

class UserRandomText():
  def __init__(self, label_number, total_labels, length):
    self.dataset = []
    self.label = [0 for i in range(0, total_labels)]
    self.label[label_number] = 1
    self.i = 0
    self.length = length
    self.unpartitioned = ""
  
  def generateNText(self, occurences, string_length):
    curText = input(f"Type {occurences * string_length}")

    while occurences > 0:
      if self.i+self.length < len(curText):
        if self.splitUserInput(curText, string_length):
          occurences -= 1
      else:
        curText = input(f"Type {occurences * string_length}")
        self.i = 0
    
  
  def splitUserInput(self, curText, string_length):
      counted = 0
      string = ""
      while counted < self.length and self.i < len(curText):
        if ord(curText[self.i]) < 97 or ord(curText[self.i]) > 122:
          self.i+=1
          continue
        else:
          string += curText[self.i]
          self.i+=1
          counted += 1

      if counted == self.length:
        self.unpartitioned += string
        self.dataset.append(string)
        return True
      
      else:
        return False

Когда данные готовы, мы можем приступить к изучению того, как мы можем экстраполировать тенденции на основе этих данных.

Методы подхода:

  1. ) Двоичная классификация с машинным обучением

Ах, великая мудрость черного ящика. Зачем искать тенденции самостоятельно, если вы можете регрессировать параметры из алгоритма машинного обучения, который, как вы не знаете, на самом деле обучается или просто запоминает числа?

Но в любом случае, именно так я подошел к алгоритму ML.

Переоснащение:

Вот где я ожидал самых больших проблем для алгоритма ML. Что касается случайности, трудно сказать, действительно ли алгоритм сходится или он просто переобучается для обучающих данных. И когда мой алгоритм впервые обучился, я сразу заметил трудности с получением точных результатов для моего тестового набора данных. По всему набору тестовых данных алгоритм может производить точную двоичную классификацию, но для конкретных M подстрок возникают трудности с получением точных прогнозов. Вот несколько способов, которыми я пытался остановить переоснащение.

Разделение моего набора данных на большее количество элементов с меньшей длиной. Из-за разделимого характера моего набора данных нет внутренней разницы между 2 строками длины 5 или 5 строками длины 2. Однако может быть немного сложно различить разницу между двумя подстроками длиной 2 или даже 3. Возможное пространство подстрок длиной 3 составляет всего 19 683. Даже для меньшего набора данных это не та шкала, которую можно легко сопоставить. Однако, если вы разделите свой набор данных на слишком длинные строки, вы начнете замечать, что ваша точность снизится для вашего набора проверки. Даже с регуляризатором L2 и уменьшением веса быстро становится очевидным, что модель будет переназначаться на слишком большие строки. Таким образом, я выбрал строку длиной около 5, чтобы избежать худшего из обоих миров.

Обучение для минимизации потерь при проверке вместо потерь при обучении. Если вы знаете ML, это довольно просто. Вместо обучения в течение эпох до тех пор, пока потери при обучении не достигнут определенного порога, я просто продолжал выполнять цикл обучения, пока потери при проверке не достигли более высокого порога. Это также имеет дополнительное преимущество, заключающееся в остановке до того, как убыток при тестировании начнет расти.

Регуляторы и уменьшение веса. Я уже упоминал об этом ранее, но убедиться, что параметров не слишком много, чтобы обобщить тенденции, можно повысить точность тестирования.

Вот моя общая реализация:

learning_rate = 0.0001
epochs = 700
model = Net(input_shape=input_length)
optimizer = torch.optim.SGD(model.parameters(),lr=learning_rate, weight_decay= 1e-5)
loss_fn = nn.BCELoss()

losses = []
accur = []
training_size = 3600
testing_loss = torch.FloatTensor([1000])
i = 0
while testing_loss.data > 0.30:
  for j in range(0, training_size):
    
    x_train = x[j]
    y_train = y[j]


    output = model(x_train)
    output = output.unsqueeze(1)

    loss = loss_fn(output,y_train.reshape(-1,1))

    predicted = model(torch.tensor(x,dtype=torch.float32))

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

  if i%50 == 0:
    losses.append(loss)
    print("test loss: {}".format(testing_loss))
    print("epoch {}\tloss : {}".format(i,loss))
  
  test_predicted = model(x[training_size::])
  testing_loss = loss_fn(test_predicted, y[training_size::])
  i+=1

В целом, если вы усредните результаты тестового набора данных, эта модель работает достаточно хорошо. Однако, как упоминалось ранее, для одной подстроки длиной 5 возникают трудности с предсказанием правильного результата. В целом неплохо, но точно не лучший.

2.) Эвристический анализ с использованием средних использованных букв

Вероятно, это то решение, о котором вы сразу подумали, когда задали этот вопрос, и я вас не виню. Это самый простой и простой способ проверить случайность человека и машины. В конце концов, мы в основном проверяем, являются ли сгенерированные машиной случайные числа случайными числами.

Чтобы реализовать это, я усреднил каждую букву в обучающем тексте для метки и сохранил это среднее значение в списке для последующего доступа. После добавления всех наших меток теперь мы можем проверить новый текст, чтобы увидеть, какая метка будет предсказана. Я просто выполнил поэлементное вычитание и суммирование результирующего массива, чтобы проверить, насколько новый текст будет отличаться от обучающих данных. Я решил просто вычислить ошибку суммы, а не ошибку суммы в квадрате, поскольку средние значения представляют собой небольшие числа от 0 до 1.

Вот реализация:

class IdentifyClass():
  def __init__(self, labels):
    self.num_labels = labels
    self.inputs = []

  def inputNewLabel(self, text):
    averages = np.zeros(26)
    for ele in text:
      ind = ord(ele)-97
      averages[ind] += 1
    averages = averages/len(text)
    self.inputs.append(averages)

  def checkExistingLabel(self, text):
    averages = np.zeros(26)
    min_diff_squared = 1000000
    min_label = -1
    for ele in text:
      ind = ord(ele)-97
      averages[ind] += 1
    for idx, label in enumerate(self.inputs):
      diff = np.subtract(label, averages)
      diff_squared = np.sum((np.absolute(diff)))

      if diff_squared < min_diff_squared:
        min_label = idx
        min_diff_squared = diff_squared
    
    return min_label

Как и ожидалось, эта эвристика работает очень хорошо. Для машинной метки почти гарантировано ее правильное предсказание только потому, что основная генерация букв просто обеспечивает равномерное распределение. Однако разница между машиной и человеком определенно заметна, как показано на этом графике.

3.) Эвристический анализ с использованием расстояния от исходного ряда

Это немного более интересный анализ, который я хотел попробовать. Это работает подобно последней эвристике, но немного отличается тем, что мы пытаемся просмотреть расстояние, пройденное пальцем, а не фактические нажатия букв.

То, как я подошел к этому, было основано на том, что я считаю, что люди склонны полагаться на указательный и средний пальцы при наборе текста. Таким образом, я создал отображение, которое использовало этот факт, придавая больший вес буквам, которые, я думаю, человек переоценил бы больше, чем компьютер. Вот отображение:

self.weight_dict = {"f": 1, "d": 2, "s": 3, "a": 4, "j":1, "k":2,
 "l":3, "q": 4*2, "w": 3*2, "e": 2*2, "r": 2, "t": 2.5, "y":2.5, 
"u":2, "i": 2*2, "o": 3*2, "p": 4*2, "z": 2.5*4, "x": 2.5*3, "c": 2.5*2, 
"v": 2.5, "b": 2.5, "n": 2.5 * 2, "m": 2.5*3}

По сути, буквы «f» и «j» имеют наименьший вес, поскольку они находятся в домашнем ряду и на них нажимают указательными пальцами. «d» и «k» также находятся в домашнем ряду, но используются средним пальцем, поэтому они имеют немного больший вес. После этого на буквы, которые не находятся в домашнем ряду, будет влиять множитель, основанный на том, какой палец из домашнего ряда щелкнет эту букву. Например, «w» нажимается безымянным пальцем в верхнем ряду. Таким образом, он имеет множитель, в 2 раза превышающий обычное значение безымянного пальца домашней строки, равное 3. Для нижней строки я решил поставить немного больший вес 2,5, так как я полагал, что люди с меньшей вероятностью будут печатать в нижней строке. , хотя я считаю, что это можно было бы изменить еще выше, чтобы продемонстрировать различия между человеком и машиной. Ниже приведена полная реализация.

class FingerDetect(IdentifyClass):
  def __init__(self, labels):
    super().__init__(labels)
    self.weight_dict = {"f": 1, "d": 2, "s": 3, "a": 4, "j":1, "k":2, "l":3, "q": 4*2, "w": 3*2, "e": 2*2, "r": 2, "t": 2.5, "y":2.5, "u":2, "i": 2*2, "o": 3*2, "p": 4*2, "z": 2.5*4, "x": 2.5*3, "c": 2.5*2, "v": 2.5, "b": 2.5, "n": 2.5 * 2, "m": 2.5*3}
 
  
  def inputNewLabel(self, text):
    total_weight = 0
    for ele in text:
      if ele in self.weight_dict:
        weight = self.weight_dict[ele]
        total_weight += weight
    average_weight = total_weight/len(text)
    self.inputs.append(average_weight)
  
  def checkExistingLabel(self, text):
    total_weight = 0
    min_val = None
    min_label = None
    for ele in text:
      if ele in self.weight_dict:
        weight = self.weight_dict[ele]
        total_weight += weight
    average_weight = total_weight/len(text)
    print(average_weight)
    
    for idx, label in enumerate(self.inputs):
      diff = abs(average_weight - label)
      if min_val is not None:
        if diff < min_val:
          min_val = diff
          min_label = idx
      else:
        min_val = diff
        min_label = idx
    return min_label

И хотите верьте, хотите нет, но это действительно сработало очень хорошо! Средний человеческий показатель для этого метода был значительно ниже, чем у машин. Это было около 2,6, что показало чрезмерную зависимость от значений, расположенных рядом с домом. Это намекает на потенциальное нормальное распределение набора текста, которое люди регулярно делают.

Выводы:

Основная причина, по которой я хотел подойти к этой проблеме, заключалась в том, чтобы продемонстрировать, что иногда машинное обучение является скорее алгоритмом памяти, чем алгоритмом, который действительно обучается. Я подумал, что концепция использования случайности была хорошим способом проверить это, поскольку псевдопаттерны, которые появляются из-за человеческой случайности, может быть немного сложно абстрагироваться и распознать. В то время как алгоритм машинного обучения мог прилично предсказывать по большому набору данных, его точность для конкретных элементов была не так хороша, как простая эвристика. Это показывает, что в некоторых случаях использование эвристики или сочетание машинного обучения с эвристикой является лучшим показателем точности, а не зависит исключительно от машинного обучения.

Еще одна причина, по которой я взялся за этот проект, заключалась в том, чтобы показать врожденное предубеждение людей к нормальному распределению, даже несмотря на то, что поначалу нормальное распределение может быть неочевидным. В случае этого вопроса довольно просто определить распределение, основанное на двух эвристиках, между пальцами домашнего ряда и буквами, которые находятся за пределами домашнего ряда. Однако я подозреваю, что люди следуют многим нормальным распределениям в своей повседневной жизни, просто мы не осознаем их. Считаю ли я, что это имеет большое значение? На самом деле, да. Использование этих общих знаний, вероятно, является хорошим способом предсказать поведение пользователей и установить границы предсказуемости для прогнозов с более низкими ставками. Это все для этого проекта.

Вероятно, выбыл.