Repository: iterative/dvc

Issue:
Experiments: Modifying parameters in lists
## Description of requested functionality
One of the things I do in my project is define a neural network model as a list of layers in params.yaml. I'm thinking it would be useful to `dvc exp run` and tweak different aspects of the model. However, accessing parameters inside lists is currently not supported. For example:
```yaml
train:
  model:
    hidden_layers:
      - BatchNormalization
      - LSTM:
          units: 32
```
Let's say I want to change the number of LSTM units. I think I should be able to do that with `train.model.hidden_layers.1.units=64`. This currently does not work, and throws an error instead.

I know lists can be tricky, but I will also now open a preliminary PR with a very naïve change that at least allows the command to run. I'm sure more changes and tests will be needed, but I wanted to get the ball rolling :smile_cat: 

## Error Logs
```
2020-10-21 16:59:00,192 ERROR: unexpected error - 'CommentedSeq' object has no attribute 'get'
------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/dvc/main.py", line 76, in main
    ret = cmd.run()
  File "/usr/local/lib/python3.8/dist-packages/dvc/command/experiments.py", line 419, in run
    self.repo.experiments.run(
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/experiments/__init__.py", line 801, in run
    return run(self.repo, *args, **kwargs)
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/__init__.py", line 51, in wrapper
    return f(repo, *args, **kwargs)
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/experiments/run.py", line 62, in run
    return repo.experiments.reproduce_one(
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/experiments/__init__.py", line 345, in reproduce_one
    stash_rev = self.new(**kwargs)
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/experiments/__init__.py", line 38, in wrapper
    return f(exp, *args, **kwargs)
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/experiments/__init__.py", line 407, in new
    stash_rev = self._stash_exp(
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/experiments/__init__.py", line 265, in _stash_exp
    self._update_params(params)
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/experiments/__init__.py", line 309, in _update_params
    _update(data, params[params_fname])
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/experiments/__init__.py", line 299, in _update
    dict_[key] = _update(dict_.get(key, {}), value)
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/experiments/__init__.py", line 299, in _update
    dict_[key] = _update(dict_.get(key, {}), value)
  File "/usr/local/lib/python3.8/dist-packages/dvc/repo/experiments/__init__.py", line 299, in _update
    dict_[key] = _update(dict_.get(key, {}), value)
  [Previous line repeated 1 more time]
AttributeError: 'CommentedSeq' object has no attribute 'get'
------------------------------------------------------------
2020-10-21 16:59:00,284 DEBUG: Version info for developers:
DVC version: 1.8.4 (pip)
---------------------------------
Platform: Python 3.8.0 on Linux-5.4.0-51-generic-x86_64-with-glibc2.27
Supports: http, https, s3
Cache types: hardlink, symlink
Repo: dvc, git
```


Discussion:

Resolving patch:
diff --git a/dvc/repo/experiments/__init__.py b/dvc/repo/experiments/__init__.py
--- a/dvc/repo/experiments/__init__.py
+++ b/dvc/repo/experiments/__init__.py
@@ -318,8 +318,14 @@ def _update_params(self, params: dict):
         # recursive dict update
         def _update(dict_, other):
             for key, value in other.items():
+                if isinstance(dict_, list) and key.isdigit():
+                    key = int(key)
                 if isinstance(value, Mapping):
-                    dict_[key] = _update(dict_.get(key, {}), value)
+                    if isinstance(dict_, list):
+                        fallback_value = dict_[key]
+                    else:
+                        fallback_value = dict_.get(key, {})
+                    dict_[key] = _update(fallback_value, value)
                 else:
                     dict_[key] = value
             return dict_


Test patch:
diff --git a/tests/func/experiments/test_experiments.py b/tests/func/experiments/test_experiments.py
--- a/tests/func/experiments/test_experiments.py
+++ b/tests/func/experiments/test_experiments.py
@@ -93,6 +93,35 @@ def test_update_with_pull(tmp_dir, scm, dvc, mocker):
         assert exp_scm.has_rev(rev)
 
 
+@pytest.mark.parametrize(
+    "change, expected",
+    [
+        ["foo.1.baz=3", "foo: [bar: 1, baz: 3]"],
+        ["foo.0=bar", "foo: [bar, baz: 2]"],
+        ["foo.1=- baz\n- goo", "foo: [bar: 1, [baz, goo]]"],
+    ],
+)
+def test_modify_list_param(tmp_dir, scm, dvc, mocker, change, expected):
+    tmp_dir.gen("copy.py", COPY_SCRIPT)
+    tmp_dir.gen("params.yaml", "foo: [bar: 1, baz: 2]")
+    stage = dvc.run(
+        cmd="python copy.py params.yaml metrics.yaml",
+        metrics_no_cache=["metrics.yaml"],
+        params=["foo"],
+        name="copy-file",
+    )
+    scm.add(["dvc.yaml", "dvc.lock", "copy.py", "params.yaml", "metrics.yaml"])
+    scm.commit("init")
+
+    new_mock = mocker.spy(dvc.experiments, "new")
+    dvc.experiments.run(stage.addressing, params=[change])
+
+    new_mock.assert_called_once()
+    assert (
+        tmp_dir / ".dvc" / "experiments" / "metrics.yaml"
+    ).read_text().strip() == expected
+
+
 def test_checkout(tmp_dir, scm, dvc):
     tmp_dir.gen("copy.py", COPY_SCRIPT)
     tmp_dir.gen("params.yaml", "foo: 1")